剑指Offer笔记

翻转链表

1.利用栈的先进后出 先将所有的节点加入到栈中 然后一个一个弹出

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null) return null;
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = new ListNode();
        cur = head;
        while(cur!=null){
            stack.push(cur);
            cur = cur.next;
        }
        //因为之前已经判断了head不会为null 所以栈中一定有数据 所以可以直接取数据
        ListNode temp =  stack.pop();
        //把当前顶端的这个节点的地址赋值给head 后面直接返回head就可以了
        //temp此时就是工作指针了
        head = temp;
        while(!stack.isEmpty()){
            //如果栈不为空 就不断的弹出栈 将这个node加载到temp的后面
            ListNode node =  stack.pop();
            temp.next = node;
            temp = temp.next;
        }
        //最后一定要记得加上这个next =null 代表链表的尾部 否则会报错
        temp.next = null;
        return head;
    }
}
  1. 迭代
public ListNode reverseList(ListNode head) {
    //首先定义prev指向当前的节点的上一个节点
    //定义当前的节点指向head cur为工作指针
    ListNode prev = null;
    ListNode cur = head;
    while(cur!=null){
        //如果工作指针不为空 那么就将cur.next 保存下来 
        //因为后面需要把cur的next反向 所以下保存下来
        ListNode temp = cur.next;
        //将当前节点的next指向前一个节点 完成翻转
        cur.next = prev;
        //当前节点设置为prev
        prev = cur;
        //保存的下一个节点的值设置为cur
        cur = temp;
    }
    //最后跳出循环的时候是因为cur=null 所以返回的值是cur的上一个节点 也就是prev
    return prev;
}
删除链表中倒数第K个节点
public ListNode removeNthFromEnd(ListNode head, int n) {
    if(head == null) return null;
    ListNode fast = new ListNode();
    ListNode slow = new ListNode();
    slow = head;
    fast = slow;
    for(int i=0;i<n;i++){
        // if(fast==null) return null;
        fast = fast.next;
    }
    //如果是null则说明往后面移动了链表长度这么多 则说明倒数第n个刚好就是头结点 就是说删除头结点
    if(fast==null) return head.next;// 说明删除的是头结点
    while(fast.next!=null){ //这里一定要考虑fast.next是否为空 防止产生空指针异常
        fast =fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return  head;
}
LRU缓存的设计
  1. 自己实现双向链表和哈希表
class LRUCache {
   //双向链表的节点定义 包括节点的k v 还有指向前一个节点和后一个节点的next 和prev
   class Node{
       Node next,prev;
       int key,value;
       public Node(int key,int value)
       {
           this.key = key;
           this.value = value;
       }
   }
   //双向链表定义
   class DoubleList{
       //记录链表大小的size 以及链表的头部和尾部 节点都是在头部和尾部中间的
       int size;
       Node head = new Node(0,0);
       Node tail = new Node(0,0);
       public DoubleList(){
           size = 0;
           head.next = tail;
           tail.prev = head;
       }
       //添加到头部
       public void addFirst(Node n){
           Node headNext = head.next;
           head.next = n;
           headNext.prev = n;
           n.prev = head;
           n.next = headNext;
           size++;
       }
       //删除最后一个元素
       public Node RemoveLast(){
           Node last = tail.prev;
           Remove(last);
           return last;
       }
       //删除元素
       public void Remove(Node node){
           node.prev.next = node.next;
           node.next.prev = node.prev;
           size --;
       }
       public int size(){
           return size;
       }
   }
   //HashMap可以加快查找的速度 K是node中的key value是一个链表中的节点
   private HashMap<Integer,Node> map;
   private DoubleList list;
   private int capacity; 
   public LRUCache(int capacity) {
       this.capacity = capacity;
       map = new HashMap<>();
       list = new DoubleList();
   }   
   public int get(int key) {
       //首先看是否存在map中 如果不存在说明没有
       //如果存在的话就返回这个值 并且插入到头部中 表示最近被访问过
       if(!map.containsKey(key)) return -1;
       int val =  map.get(key).value;
       put(key,val);
       return val;
   } 
   //对双向链表进行删除和增加都要记得将map中的key重新更新
   public void put(int key, int value) {
       // 先把新节点 x 做出来
       Node x = new Node(key, value);
       //如果存在Key    
       if (map.containsKey(key)) {
           // 删除旧的节点,新的插到头部
           list.Remove(map.get(key));
           list.addFirst(x);
           // 更新 map 中对应的数据
           map.put(key, x);
       } else { //不存在的时候要考虑是否超过了容量 超的话需要移除最后一个节点
           if (capacity == list.size()) {
               // 删除链表最后一个数据
               Node last = list.RemoveLast();
               map.remove(last.key);
           }
           // 直接添加到头部
           list.addFirst(x);
           map.put(key, x);
       }
   }
}
合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if(l1==null) return l2;
    else if (l2==null) return l1;
    else if(l1.val<l2.val) {
        l1.next = mergeTwoLists(l1.next,l2);
        return l1;
    }
    else{
        l2.next =  mergeTwoLists(l1,l2.next);
        return l2;
    }   
}


//非递归形式
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1==null) return l2;
    if (l2==null) return l1;
    ListNode head= new ListNode(-1);
    ListNode pre = head;
    while (l1!=null && l2!=null){
        int val1 = l1.val;
        int val2 = l2.val;
        if (val1 <= val2){
            pre.next = l1;
            l1 = l1.next;
        }
        else {
            pre.next = l2;
            l2 = l2.next;
        }
        pre = pre.next;
    }
    if (l1!=null) pre.next  = l1;
    if (l2!=null) pre.next  = l2;

    return head.next;

}
56 - I. 数组中数字出现的次数 (中等)

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。 输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

class Solution {
    //如果数组中只存在一个不相同的数 那么直接依次做与运算 就可以得到结果
    //但是这里有两个数字 那么可以按照一定的规则分成两部分 这个规则要求将这两个不同的数字分到两边
    public int[] singleNumbers(int[] nums) {
       //异或也存在交换率 也就是说 即使位置不相邻 相同的数最后还是会归为0
       int temp=0;
       for(int num : nums){
             temp^=num;
       }
       //根据上面的一轮异或操作之后 temp中存的值就是两个不相同的数字的异或结果
       //这个结果肯定是有一位为1的,在为1的这一位中 两个不相同的数字 一个为0 一个为1 (在当前这个位置上的是时候)
       //那么就可以以这个位置为一个标准 对数组进行划分 这两个数字肯定会被划分到两边 同时 剩下的数字都是相同的 那么肯定会被分到同一边去
       //在分割的两边分别进行异或操作 最后得到两个结果 就是两个不相同的值
       int mask = 1;
       //找到最低为的那个1 也就是这两个数字在该位开始不一样了 那么就可以以该为为标准做&运算
       while((temp&mask)==0){ 
           mask<<=1;//从1开始依次左移
       }

       int a=0,b=0;
       for(int num : nums){
           if((mask&num)==0){ //&为0 说明这个元素在那个位置上是0 就分到一边 下面说明这个位置是1 分到另一边
               a^=num;
           }
           else
             b^=num;
       }
       return new int[]{a,b};
    }
}
剑指 Offer 56 - II. 数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4
链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof

class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i <nums.length ; i++) {
            if(map.containsKey(nums[i])) {
                int val = map.get(nums[i]);
                val++;
                map.put(nums[i],val);
            }
            else
                map.put(nums[i],1);
        }
        for (Map.Entry<Integer,Integer> entry : map.entrySet()){
            if (entry.getValue()==1){
                return entry.getKey();
            }
        }
        return -1;
    }
}
剑指 Offer 45. 把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: “102”
示例 2:

输入: [3,30,34,5,9]
输出: “3033459”
链接:https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof

  public String minNumber(int[] nums) {
      //定义一个小根堆 比较的规则就是交换位置后两个数的大小 这里的泛型要定义为String 不然是integer的话比较久没有意义
    Queue<String> queue = new PriorityQueue<String>(new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return (o1+o2).compareTo(o2+o1);
        }
    });
    //遍历数组中的数据 然后加入到优先队列 由于有限队列中存放的是String所以这里要加上""
    for (int num:nums){
        queue.add(num+"");
    }
    StringBuilder sb = new StringBuilder();
    while (!queue.isEmpty()){
        sb.append(queue.poll());
    }
    return sb.toString();
}
剑指 Offer 47. 礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向 下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物。

class Solution {
    //思路分析
    //要获得价值的最大值 可以考虑使用动态规划 由于走的方向只有向右和向下 所以 最左边的一列和最上面的一行的数据时直接可以通过初始化的数组得到答案
    //其他的包含在中间的位置的最大路径可以通过两个方向得到 首先是通过它上面的位置往下移动 或者是通过左边的位置向右移动 选择二者之间较大的那一个 
    //每次选取的都是较大值 那么最后得到的就是最大的
    public int maxValue(int[][] grid) {
        int m =  grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        //竖排的值初始化
        for (int i = 1; i <m ; i++) {
            dp[i][0] = dp[i-1][0]+grid[i][0];
        }
        //横排的初始化
        for (int i = 1; i < n; i++) {
            dp[0][i] = dp[0][i-1]+grid[0][i];
        }
        for (int i = 1; i <m ; i++) {
            for (int j = 1; j <n ; j++) {
                dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
}
剑指 Offer 63. 股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

class Solution {
    // 定义一个dp数组 数组dp[i]表示的是以prices[i]结尾的数的最大利润
    //最大利润 就是当前这一天的价值减去前面的最小值 和 前一天的最大利润 中的最大的那一个
    public int maxProfit(int[] prices) {
        //如果长度小于等于1 利润为0
        if(prices.length <= 1) return 0;
        //初始化最小值就是第一个数
        int min = prices[0];
        //定义同样大小的数组代表每一天的最大利润
        int[] dp = new int[prices.length];
        //初始化第一天的利润为0
        dp[0] = 0;
        //遍历数组 不断的更行最小值和dp[i]
        for(int i =1;i<prices.length;i++){
            min = Math.min(prices[i],min); //最小值更新
            dp[i] = Math.max(prices[i]-min,dp[i-1]); //更新每一天的最大值 从当前这天的值减去最小值和 前一天的利润中找最大的那一个
        }
        return dp[prices.length-1];
    }
}
剑指 Offer 12. 矩阵中的路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wgVfa1SN-1648528653810)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20210910155247353.png)]

class Solution {
    public boolean exist(char[][] board, String word) {
        //将整个二维数组的行列求出来
        int i = board.length;
        int j = board[0].length;
        for (int k = 0; k < i ; k++) {
            for (int l = 0; l <j ; l++) {
                //遍历整个二维数组,从0,0出开始 如果从某一个位置开始遍历过后返回为true说明有路径 就直接返回 否则的话 就继续在下一个位置开始搜索
                if(dfs(board,k,l,word,0)) return true;
            }
        }
        return false;
    }
    //回溯搜索
    //传入的参数包括有 字符数组 以及当前的位置(行,列) 字符串 字符串的字符索引位置
    private boolean dfs(char[][] board, int i, int j, String word, int k) {
            char[] ans = word.toCharArray();
            //先判断边界条件 什么时候这个函数会返回false 具体而言就是数组的下标越界或者不符合规定  或者是当前位置时候 字符串的字符和字符数组中的值          不相同
           if(i>=board.length || i<0 || j>=board[0].length || j<0 || board[i][j]!=ans[k]) return false;
           //如果各方面符合 就判断当前这个位置的索引是否已经是字符串的最后一个位置 如果是就返回true 说明存在路径
           if(k ==  word.length()-1) return true;
           //如果还是不符合 就继续向周围扩散搜索 这时候由于这个位置已经搜索过了 所以要把这个位置设置为访问了 并且要保存这个位置的字符 
           // 因为这次搜索不成功的话 不能影响下一次的搜索 所以在返回之前 要把这个值设置回来 
           char temp = board[i][j];
           board[i][j] = '\0';
           boolean res = dfs(board,i-1,j,word,k+1)||dfs(board,i+1,j,word,k+1)||dfs(board,i,j-1,word,k+1)||dfs(board,i,j+1,word,k+1);
           board[i][j] = temp;
           return res;
    }
}
剑指 Offer 35. 复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

class Solution {
    public Node copyRandomList(Node head) {
        if(head==null) return null;
        //声明一个工作指针
        Node cur = head;
        //map用来保存这个链表节点 单纯的实现链表节点的复制就是靠map
        Map<Node,Node> map = new HashMap<>();
        //当节点不为空的时候就不断进行插入 实现链表节点的复制
        while(cur!=null){
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }
        cur = head; //将工作指针恢复到head的状态
        while(cur!=null){
            //将原节点的next和random给新链表
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(head);
        
    }
}
剑指 Offer 36. 二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

class Solution {
    Node head, pre;
    public Node treeToDoublyList(Node root) {
        if(root==null) return null;
        dfs(root);

        pre.right = head;
        head.left =pre;//进行头节点和尾节点的相互指向,这两句的顺序也是可以颠倒的

        return head;

    }

    public void dfs(Node cur){
        if(cur==null) return;
        dfs(cur.left);

        //pre用于记录双向链表中位于cur左侧的节点,即上一次迭代中的cur,当pre==null时,cur左侧没有节点,即此时cur为双向链表中的头节点
        if(pre==null) head = cur;
        //反之,pre!=null时,cur左侧存在节点pre,需要进行pre.right=cur的操作。
        else pre.right = cur;
       
        cur.left = pre;//pre是否为null对这句没有影响,且这句放在上面两句if else之前也是可以的。

        pre = cur;//pre指向当前的cur
        dfs(cur.right);//全部迭代完成后,pre指向双向链表中的尾节点
    }
}


class Solution {
    Node head,pre;
    public Node treeToDoublyList(Node root) {
        // 边界条件
        if(root ==null) return root;
        //执行中序遍历
        dfs(root);
        //上述的中序遍历完成之后 head指向头节点 pre指向的是尾节点
        //这时候需要将两个节点连接起来
        pre.right = head;
        head.left = pre;
        //返回头节点
        return head;
    }
    public void dfs(Node root){
        //中序遍历 只不过现在不是打印根节点 而是将根节点转换为链表的形式
        if(root == null) return ;
        //遍历左子树
        dfs(root.left);
        //操作根节点
        //如果是null 说明是刚开始 这时候让head指向它
        if(pre == null) head = root;
        //一般情况 修改指向
        else pre.right = root;
        root.left = pre;
        pre = root;

        //遍历右子树
        dfs(root.right);
    }
}
剑指 Offer 32 - III. 从上到下打印二叉树 III

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-frjIlqvP-1648528653812)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20210912150331446.png)]

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        if(root!=null){
              queue.add(root);
        }
        int k = 1;
        while(!queue.isEmpty()){
            //这里使用了双端队列 也就是说可以在两边进行插入删除操作 这样就可以实现奇数偶数行的不同方向
            //如果是奇数行就顺序插入 也就是插入到后面 如果是偶数的话就从头插入即可
            LinkedList<Integer> temp  = new LinkedList<>();
            //队列里面的都是当前这一行的所有元素 遍历这一行的所有元素 
            for(int i=queue.size();i > 0 ;i --){
                TreeNode node = queue.poll();
                if(k%2==1) { //判断奇偶
                    temp.addLast(node.val);
                }
                else temp.addFirst(node.val);
                if(node.left!=null) queue.add(node.left);
                if(node.right!=null) queue.add(node.right);
            }
            res.add(temp);
            k++;
        }
        return res;

    }
}
剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

由于是不限制输出顺序的 所结果集可以使用Set来进行存储

class Solution {
    public  String[] permutation(String s) {
        //来标识字符是否被访问过
        boolean[] visited = new boolean[s.length()];
        //初始的时候全部未访问
        Arrays.fill(visited,false);
        //将字符串转换为字符数组
        char[] chars = s.toCharArray();//存储每一次的结果
        String temp ="";
        //存储最后返回的结果 
        Set<String> res = new HashSet<>();
        backtrack(chars,temp,visited,res);
        //将Set转换为数组 数组的大小为res的size
        return res.toArray(new String[res.size()]);
    }

    private  void backtrack(char[] chars, String temp, boolean[] visited, Set<String> res) {
        //结束条件 如果存储临时变量的数组大小和原数组一样 就结束
        if(chars.length == temp.length()){
            res.add(temp);
            return;
        }
        //遍历整个数组
        for (int i = 0; i < chars.length; i++) {
            //如果被访问过 就继续下一次循环
            if(visited[i]) continue;
            //将当前的位置设置为访问过
            visited[i]=true;
            //继续下一次的回溯
            backtrack(chars,temp+chars[i],visited,res);
            //回溯结束 撤销选择 设置为未访问
            visited[i]=false;
        }
    }
}

回溯法模板

private void backtrack("原始参数") {
    //终止条件(递归必须要有终止条件)
    if ("终止条件") {
        //一些逻辑操作(可有可无,视情况而定)
        return;
    }

    for (int i = "for循环开始的参数"; i < "for循环结束的参数"; i++) {
        //一些逻辑操作(可有可无,视情况而定)

        //做出选择

        //递归
        backtrack("新的参数");
        //一些逻辑操作(可有可无,视情况而定)

        //撤销选择
    }
}

剑指 Offer 59 - II. 队列的最大值

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uh1bmtJ5-1648528653812)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220301194551182.png)]

可以维护两个双端队列,这两个队列一个是普通的队列就是用来保存进出的元素。对获取最大值这个操作单独创建一个队列,这个队列只是用来保存最大值。

  • push_back() 元素入队,这时候普通的队列直接入队。对于最大值队列就需要将之前的最大值与当前的进行比较。设想如果这个元素比之前的最大值大,那么在这个元素之前进行的取最大值都是这个元素,那么这时候就可以把原来队列中的元素全部取出来。只放当前这个值。如果碰到比它大的值就直接加入就行 while (!MaxValueQueue.isEmpty() && MaxValueQueue.peekLast() < value) MaxValueQueue.addLast(value);
  • pop_front() 弹出元素。弹出元素涉及到两个队列的弹出。对于普通的元素就直接弹出。对于最大值队列。只要当前弹出的元素不是最大值都可以不用管。如果是最大值的话就需要将最大值的这个也弹出。
class MaxQueue {

    Deque<Integer> queue, MaxValueQueue; //双端队列 

    public MaxQueue() {
        queue = new LinkedList<>();
        MaxValueQueue = new LinkedList<>();
    }

    public int max_value() {
        if (queue.isEmpty()) return -1;
        return MaxValueQueue.peekFirst();

    }

    public void push_back(int value) {
        queue.addLast(value);
        while (!MaxValueQueue.isEmpty() && MaxValueQueue.peekLast() < value) {
            MaxValueQueue.removeLast();
        }
        MaxValueQueue.addLast(value);

    }

    public int pop_front() {
        if (queue.isEmpty()) return -1;
        if (queue.peekFirst().equals(MaxValueQueue.peekFirst())) {
            MaxValueQueue.removeFirst();
            return queue.pollFirst();
        } else {
            return queue.pollFirst();
        }
    }
}

/**
         * Your MaxQueue object will be instantiated and called as such:
         * MaxQueue obj = new MaxQueue();
         * int param_1 = obj.max_value();
         * obj.push_back(value);
         * int param_3 = obj.pop_front();
         */
剑指 Offer 66. 构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MONncbcZ-1648528653813)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220302203722067.png)]

题目中规定了不能使用除法,这里最终的这个数组表示的是除开当前这个位置的其余元素的乘积,这时候可以将这个乘积分成两部分,一部分是当前元素的左边部分的所有乘积,一部分是当前元素的右边的所有乘积。最后对结果数组赋值的时候就是直接将当前元素的左右两边进行相乘就是最后的结果。

class Solution {
    public int[] constructArr(int[] a) {
        //特殊情况 为空或者长度为0
        if (a == null) return null;
        if(a.length==0) return a;
        
        int[] leftRes = new int[a.length];
        int[] rightRes = new int[a.length];
        leftRes[0] = 1;
        rightRes[a.length - 1] = 1;
        //更新左右结果数组
        for (int i = 1; i < a.length; i++) {
            leftRes[i] = leftRes[i - 1] * a[i - 1];
        }
        for (int i = a.length - 2; i >= 0; i--) {
            rightRes[i] = rightRes[i + 1] * a[i + 1];
        }
        //更新最后的结果数组
        int[] res = new int[a.length];
        for (int i = 0; i < a.length; i++) {
            res[i] = leftRes[i] * rightRes[i];
        }
        return res;
    }
}
剑指 Offer 35. 复杂链表的复制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utZadG3D-1648528653813)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220306143119192.png)]

public class _35复杂链表的复制 {
    public Node copyRandomList(Node head) {
        //因为链表具有随机的一个指向 直接复制就可能导致一个问题
        //有可能当前复制的那个新链表的一个指向还没有生成
        //所以先进行链表节点的一对一复制 将整个链表的节点复制出来
        //然后遍历原来的节点 并且对新节点进行赋值 新节点的random就是新节点 所以后面进行复制的时候是get(cur.next)而不是直接的cur.next
        if (head==null) return null;
        Map<Node,Node> map = new HashMap<>();
        Node cur = head;
        while (cur!=null){
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }
        cur = head;
        while (cur!=null){
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur=cur.next;
        }
        return map.get(head);
    }
}
剑指 Offer 53 - I. 在排序数组中查找数字 I

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idR4GPQE-1648528653813)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220308181747919.png)]

public class _53在排序数组中查找重复的数字 {
    public int search(int[] nums, int target) {
        //不断缩短二分查找的区间
        //找到target之后继续将区间收缩
        int left = 0;
        int right = nums.length-1;
        int mid;
        while (left<=right){
            mid = (left+right)/2;
            if (nums[mid]>target){
                right = mid-1;
            }
            else if (nums[mid]<target) {
                left = mid+1;
            }
            else{
                if (nums[left]!=target) {
                    left++;
                }
                else if (nums[right]!=target){
                    right--;
                }
                else break;
            }
        }
        return right-left+1;
    }

    public static void main(String[] args) {

    }
}
剑指 Offer 11. 旋转数组的最小数字

class Solution {
    public int minArray(int[] numbers) {
        if (numbers.length==0 || numbers==null) return -1;
        if (numbers.length==1) return numbers[0];
        int left = 0;
        int right = numbers.length-1;
        int  mid;
        while(left < right){
            mid = (left+right)/2;
            if (numbers[mid] <numbers[right]) right = mid;
            else if (numbers[mid] > numbers[right]) left = mid+1;
            else right--;
        }
        //最后退出循环的时候的条件是 left == righ此时的中点就是left
        return numbers[left];
    }
}
面试题50. 第一个只出现一次的字符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U3XZAD6E-1648528653814)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220309160152040.png)]

public class _50第一个只出现一次的字符 {
    public char firstUniqChar(String s) {
        if (s==null || s.length()==0) return ' ';
        Map<Character,Boolean> map = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            //这种方式添加是错误的 因为如果里面之前没有添加元素的话就是null 就不可以获取
            //所以采取后者 直接判断里面包不包含某个key 对其取反
//            if (map.get(s.charAt(i))) map.put(s.charAt(i),false);
//            else map.put(s.charAt(i),true);
            map.put(s.charAt(i),!map.containsKey(s.charAt(i)));
        }
        for (int i = 0; i < s.length(); i++) {
            if (map.get(s.charAt(i))) return s.charAt(i);
        }
        return ' ';


    }
    public static void main(String[] args) {

    }
}
剑指 Offer 26. 树的子结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZJ2CSvB-1648528653814)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220311164241666.png)]

public class _26树的子结构 {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        //边界情况 如果任何一个是空的 就返回false
        if (A==null || B == null) return false;
        //其他情况 判断这时候B这个子树是不是原来这个树的根节点或者左子树 右子树的子树
        else return (recur(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right, B)  );

    }

    private boolean recur(TreeNode A, TreeNode B) {
        //判断以这两个节点开始的两个子树是不是一样的 首先对于B来说 如果为null的话说明已经判断完了 这时候他就是别的树的子树
        //对于A来说 如果为NUll的话说明B还没有判断完全 这时候return false
        //如果这两个节点相同的话 就应该判断左右子树是不是一样的 就这样逐渐的递归
        if (B==null) return true;
        if (A==null || A.val !=B.val) return false;
        return recur(A.left,B.left) && recur(A.right,B.right);
    }

    public static void main(String[] args) {

    }
}
剑指 Offer 28. 对称的二叉树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHoqrCyC-1648528653814)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220311170654395.png)]

public class _28对称的二叉树 {
    //判断这个根节点的二叉树是不是镜像的
    public boolean isSymmetric(TreeNode root) {
        //边界条件 为空的时候返回 true
        if(root == null) return true;
        //如果不为空的话就看左右子树是否符合 这时候需要新写一个函数判断两个节点之间是不是这种镜像关系
        else return isSy(root.left,root.right);

    }
    public boolean isSy(TreeNode left, TreeNode right){
        //首先判断边界 如果都是空的话 就返回true 如果有一个是空就不是
        if(left ==null && right ==null) return true;
        if(left == null || right == null) return false;
        //看二者的值是不是一样的 不同就直接返回 false
        if(left.val != right.val) return false;
        //如果相同的话 就判断这两个子树的左右节点是不是符合条件
        //其中 左子树和右子树比较
        return (isSy(left.left,right.right) && isSy(left.right,right.left));
    }
    public static void main(String[] args) {

    }
}

剑指 Offer 46. 把数字翻译成字符串

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pauh3WNw-1648528653815)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220314163916709.png)]

public class _46把数字翻译成字符串 {
    public static int translateNum(int num) {
        String s = Integer.toString(num);
        int[] dp = new int[s.length()+1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <=s.length(); i++) {
            //这里substring这个函数中的参数是左闭右开的 所以遍历的时候要取length 而不是length-1
            // 这种情况是以当前这个结尾的字符串的后两个数字可以组成一个新的组合 那以这个结尾的数字的种类就是
            //前一个位置结束的  +  前两个位置结束的种类
            if (Integer.valueOf(s.substring(i-2,i)) <=25 && Integer.valueOf(s.substring(i-2,i)) >=10) {
                dp[i] = dp[i-2] + dp[i-1];
            }
            else dp[i] = dp[i-1];
        }
        return dp[s.length()];
    }
    public static void main(String[] args) {
        System.out.println(translateNum(12258));
    }
}
剑指 Offer 18. 删除链表的节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvDsCWWt-1648528653815)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220315155145330.png)]

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        ListNode pre = head;
        ListNode cur = head.next;
        //单独判断要删除的元素是不是头节点
        if(head.val == val) return head.next;
        //
        while(cur!=null){
            int Tempval = cur.val;
            if(Tempval == val){
                pre.next = pre.next.next;
                return head;
            }
            else 
            pre = pre.next;
            cur = cur.next;
        }
        return head;
    }
}
剑指 Offer 52. 两个链表的第一个公共节点

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode node1 = headA;
        ListNode node2 = headB;
        //如果两个链表没有公共节点的话 可以看作是两个节点的公共节点就是null 这时候 两个节点相等 返回 null
        while(node1 != node2){//每个节点刚开始先遍历自己 遍历完成之后就遍历另外的节点 两个节点相遇的时候走过的路程是一样的 
            // 所以只要不为空的时候就一直往后走 走完之后就走另外的节点
            node1 = node1 == null? headB:node1.next;
            node2 = node2 == null? headA:node2.next;
            }
        return node1;
        
    }
}

剑指 Offer 13. 机器人的运动范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHx9VX6j-1648528653815)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220318151226147.png)]

public int getSum(int i) {
    int sum = 0;
    while (i != 0) {
        sum += i % 10;
        i = i / 10;
    }
    return sum;
}

public int movingCount(int m, int n, int k) {
    boolean[][] visited = new boolean[m][n];
    return dfs(visited,m,n,k,0,0);
}
//因为最开
private int dfs(boolean[][] visited, int m, int n, int k, int i, int j) {
    if (i >= m || j >= n || visited[i][j] || (getSum(i) + getSum(j))>k ) return 0;
    visited[i][j] = true;
    //搜索的时候只是向下面 和 右边搜索 
    //开始的时候会一直向下遍历 直到越界 这时候就会向右边遍历 如果右边不符合的话会继续回退到上一层
    //上一层继续上述操作 达到不满足的那个地方之后 后面不会继续遍历了
    return 1 + dfs(visited,m,n,k,i+1,j) + dfs(visited,m,n,k,i,j+1);
}

剑指 Offer 34. 二叉树中和为某一值的路径

class Solution {
    public LinkedList<List<Integer>> res =  new LinkedList<>();
    public LinkedList<Integer> path = new LinkedList<>();

    public void recur(TreeNode root, int target){
        //边界条件
        if(root==null) return;

        //正常进入应该做的事情
        path.add(root.val);
        target-=root.val;
        //符合题意的时候应该怎么做
        if(target==0 && root.left==null && root.right==null) {
            res.add(new LinkedList(path));
        }
        //回溯其他方向
        recur(root.left,target);
        recur(root.right,target);
        //取消选择
        path.removeLast();
    }

    public List<List<Integer>> pathSum(TreeNode root, int target) {
         recur(root,target);
         return res;
    }
}

剑指 Offer 61. 扑克牌中的顺子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfNXNrAh-1648528653816)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220320101755356.png)]

如果最终结果是顺子的话,那么最大的减去最小的肯定是小于5的。同时如果包括了相同的数字就不是顺子,

class Solution {
    public boolean isStraight(int[] nums) {
        Set<Integer> repeat = new HashSet<>();
        int max = 0, min = 14; // 这里的最大最小值要设置好 
        for(int num : nums) {
            if(num == 0) continue; // 跳过大小王
            max = Math.max(max, num); // 最大牌
            min = Math.min(min, num); // 最小牌
            if(repeat.contains(num)) return false; // 若有重复,提前返回 false
            repeat.add(num); // 添加此牌至 Set
        }
        return max - min < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
    }
}

剑指 Offer 41. 数据流中的中位数

class MedianFinder {
    Queue<Integer> min,max;


    /** initialize your data structure here. */
    public MedianFinder() {
        min = new PriorityQueue<>(); //小根堆 保存大的元素
        max = new PriorityQueue<>((x,y)->(y-x)); //大根堆 保存左边的小元素
    }

    public void addNum(int num) {
        //添加元素的时候首先判断两个堆的大小是不是一样
        // 如果两个一样 新增的元素按理来说应该保存到左边 也就是大根堆这边来
        //但是这个元素有可能比 右边的元素大 所以采取的办法就是先加到右边 再把右边的对顶元素放在左边来 这就保证了有序性
        if (max.size() == min.size()){  //大小一样 这时候往左边放 所以先进去右边 再出来
            min.add(num);
            max.add(min.poll());
        }
        else {                         //大小不一样 这时候需要两边平衡一下 而且肯定是左边多了一个 所以先去左边, 然后去右边 
            max.add(num);
            min.add(max.poll());
        }

    }

    public double findMedian() {
        if (min.size() == max.size()) return  (min.peek()+max.peek())/2.0;
        else  return max.peek(); //因为上面是将多的那一半放再左边 所以最后返回的时候也是左边 也就是大根堆
    } 
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3q0VxMca-1648528653816)(https://gitee.com/yan256992/cloudimages/raw/master/img/image-20220323162047083.png)]

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root.val < p.val && root.val < q.val) 
            return lowestCommonAncestor(root.right,p,q);
        else if(root.val > p.val && root.val > q.val)
            return lowestCommonAncestor(root.left,p,q);
        return root;
    }
}

剑指 Offer 68 - II. 二叉树的最近公共祖先

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null; // 如果树为空,直接返回null
        if(root == p || root == q) return root; // 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先)
        TreeNode left = lowestCommonAncestor(root.left, p, q); // 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁
        TreeNode right = lowestCommonAncestor(root.right, p, q); // 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁
        if(left == null) return right; // 如果在左子树中 p和 q都找不到,则 p和 q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
        else if(right == null) return left; // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况,如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
        else return root; //否则,当 left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root
    }
}

剑指 Offer 07. 重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

class Solution {
    HashMap<Integer, Integer> map = new HashMap<>();//标记中序遍历
    int[] preorder;//保留的先序遍历,方便递归时依据索引查看先序遍历的值

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        //将中序遍历的值及索引放在map中,方便递归时获取左子树与右子树的数量及其根的索引
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        //三个索引分别为
        //当前根的的索引
        //递归树的左边界,即数组左边界
        //递归树的右边界,即数组右边界
        return recur(0,0,inorder.length-1);
    }

    TreeNode recur(int pre_root, int in_left, int in_right){
        if(in_left > in_right) return null;// 相等的话就是自己
        TreeNode root = new TreeNode(preorder[pre_root]);//获取root节点
        int idx = map.get(preorder[pre_root]);//获取在中序遍历中根节点所在索引,以方便获取左子树的数量
        //左子树的根的索引为先序中的根节点+1 
        //递归左子树的左边界为原来的中序in_left
        //递归左子树的右边界为中序中的根节点索引-1
        root.left = recur(pre_root+1, in_left, idx-1);
        //右子树的根的索引为先序中的 当前根位置 + 左子树的数量 + 1
        //递归右子树的左边界为中序中当前根节点+1
        //递归右子树的右边界为中序中原来右子树的边界
        root.right = recur(pre_root + (idx - in_left) + 1, idx+1, in_right);
        return root;

    }

}

剑指 Offer 33. 二叉搜索树的后序遍历序列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JpZ1exzT-1648528653816)(C:\Users\48108\AppData\Roaming\Typora\typora-user-images\image-20220324192151425.png)]

public boolean verifyPostorder(int[] postorder) {
	return recur(postorder,0,postorder.length-1);
}

private boolean recur(int[] postorder, int i, int i1) {
	if (i >=i1) return true;
    int a = i;
    while (postorder[a] < postorder[i1]) a++;
    int p = a;
    while (postorder[a] > postorder[i1]) a++;
	return a == i1 && recur(postorder,i,p-1) && recur(postorder,p,i1-1);

}
剑指 Offer 56 - I. 数组中数字出现的次数

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

public int[] singleNumbers(int[] nums) {
    // 第一步 先将所有的元素进行亦或(不同输出1)操作
    int mask = 0;
    for (int num : nums) {
        mask = mask ^ num;
    }

    // 得到的这个元素是两个不同的元素的值拼接起来的, 但是可以通过某一位是否是1将这两个数在原来的数组中分开
    // 联想 划分奇偶的时候 判断最后1位是不是1来判断是奇数还是偶数
    // 所以第二步就是将上面得到的这个元素的最低位的1 找出来
    int m = 0;
    while ((mask & m) == 0) {
        m = m << 1;
    }
    //通过上述操作就可以得到一个值 这个值还是可以将原来数组的元素分成两部分 两部分分别亦或就可以得到最后的值
    int x = 0, y = 0;
    for (int num : nums) {
        if ((num & m) == 1) {
            x = x ^ num;
        } else y ^= num;
    }
    return new int[]{x,y};

}
剑指 Offer 14- I. 剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

    public int cuttingRope(int n) {
        if (n <= 3) return n - 1; //特殊情况、
        //dp[i] 表示的是以i为长度的绳子的能够得到的最大值
        int[] dp = new int[n + 1];
        //初始化 绳子剪这些长度的时候的最大值
        //2 3 的时候就不用继续减下去了 就是当前的最大值了
        dp[0] = dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        //开始赋值 就是将整个绳子分成两部分 两部分得到的最大值进行相乘 
        //因为分成的两部分有很多取值 所以需要再加一层循环
        for (int i = 4; i <= n; i++) {
            for (int j = 1; j <= i - 1; j++) {
                dp[i] = Math.max(dp[j] * dp[i - j], dp[i]);
            }
        }
        return dp[n];
    }
剑指 Offer 57 - II. 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

    public static int[][] findContinuousSequence(int target) {
        // 滑动窗口
        List<int[]> res = new ArrayList<>();
        //初始化左右窗口的值 以及窗口中的和值
        int left = 1;
        int right = 2;
        int sum = 3;
        // 滑动窗口 这里不可以是等于
        while (left < right) {
            // 如果相同的话 就进行赋值
            if (sum == target) {
                int[] temp = new int[right - left + 1];
                for (int i = 0; i < right - left + 1; i++) {
                    temp[i] = left + i;
                }
                res.add(temp);
            }
            //这里是 >= 因为如果走了上面的代码 后面如果不进行操作的话就会一直循环 所以开始压缩窗口
            // 这里也可以是不等于 但是上面就需要加上下面的代码了
            // 窗口压缩的时候 要先进行减操作 然后left++ 如果先left++的话就减去的是后面的那个数字了
            //同理 进行扩张的时候需要先进行加加操作 不然的话加的还是原来的right的值 相当于有两个right在里面了
            if (sum >= target) {
                sum -= left;
                left++;

            } else {
                right++;
                sum += right;
            }
        }
        return res.toArray(new int[0][]);

    }
剑指 Offer 29. 顺时针打印矩阵
import java.util.ArrayList;
import java.util.List;
import java.util.function.LongFunction;

/**
 * 功能描述:
 *
 * @Author: ygq
 * @Date: 2022/3/29 10:25
 */
public class _29顺时针打印二维数组 {
    public int[] spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        if(matrix.length ==0 || matrix[0].length==0) return new int[]{};
        int left = 0,right = matrix[0].length-1,top = 0, down = matrix.length-1;
        while (true){
            for (int i = left; i <= right; i++) {
                res.add(matrix[top][i]);
            }
            top++;
            if(top>down) break; //注意结束条件
            for (int i = top; i <= down; i++) {
                res.add(matrix[i][right]);
            }
            right--;
            if(right<left) break;
            for (int i = right; i >=left; i--) {
                res.add(matrix[down][i]);
            }
            down--;
            if(down<top) break;
            for (int i = down; i >=top ; i--) {
                res.add(matrix[i][left]);
            }
            left++;
            if(left>right) break;
        }
        int[] re = new int[res.size()];
        for (int i = 0; i < res.size(); i++) {
            re[i] = res.get(i);
        }
        return re;
    }
    public static void main(String[] args) {

    }
}

剑指 Offer 31. 栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

将pushed数组的值进行入栈操作,模拟整个过程,然后判断栈顶元素和出栈数组的值是不是一样的,一样的就出栈。

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Stack<Integer> s = new Stack<>();
        int i = 0;
        for(int num : pushed){
            s.push(num);
            while(!s.isEmpty() && s.peek() == popped[i]){
                s.pop();
                i++;
            }
        }
        return s.isEmpty();

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值