leetcode 剑指offer小结

栈与队列

剑指 Offer 09. 用两个栈实现队列

使用两个堆栈,一个输出栈,另一个输入栈。队列入队:直接将元素压入输入栈,队列出队:如果输出栈为空,将输入栈元素压入输出栈,再将输出栈元素出栈。

查看代码
class CQueue {
    private Stack<Integer> inputStack;
    private Stack<Integer> outputStack;
    public CQueue() {
        inputStack = new Stack();
        outputStack = new Stack();
    }
    
    public void appendTail(int value) {
        inputStack.push(value);
    }
    
    public int deleteHead() {
        if(!outputStack.isEmpty()){
            return outputStack.pop();
        }else{
            while(!inputStack.isEmpty()){
                outputStack.push(inputStack.pop());
            }
            return outputStack.isEmpty() ? -1 : outputStack.pop();
        }
    }
}

剑指 Offer 30. 包含min函数的栈

要求min、push 及 pop 的时间复杂度都是 O(1)。可以建立辅助栈实现。入栈:入栈元素和辅助栈顶元素比较,如果入栈元素小于等于辅助栈栈顶元素,同时将该元素入辅助栈。保持辅助栈当前栈顶元素最小。出栈:如果出栈元素与辅助栈栈顶元素相等,说明当要出的元素是最小元素,需要将辅助栈元素也出栈,保持辅助栈元素最小,如果不相等,主栈直接出栈即可。求最小数:直接返回当前辅助栈的栈顶元素的值。

Java: Integer用==比较时127相等128不相等的原因

https://stackoverflow.com/questions/1700081/why-is-128-128-false-but-127-127-is-true-when-comparing-integer-wrappers-in-ja

辅助栈做法
class MinStack {
    Stack<Integer> A, B;
    public MinStack() {
        A = new Stack<>();
        B = new Stack<>();
    }
    public void push(int x) {
        A.add(x);
        if(B.empty() || B.peek() >= x)//等于也入栈
            B.add(x);
    }
    public void pop() {
        if(A.pop().equals(B.peek()))//如果是==不通过。看一看==和equals()的区别
            //Integer类里有数组,如果小于128,直接从数组取比内存,所以相等。否则是比对象,内存地址不一样
            B.pop();
    }
    public int top() {
        return A.peek();
    }
    public int min() {
        return B.peek();
    }
}

作者:jyd
链接:https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof/solution/mian-shi-ti-30-bao-han-minhan-shu-de-zhan-fu-zhu-z/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

链表

剑指 Offer 06. 从尾到头打印链表

写着写着力扣网站崩了......

解法:如果允许改动链表,可以将链表原地逆置

链表原地逆置,再遍历
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        ListNode pre = null,cur = head;
        int count = 0;
        while(cur != null) {
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur; //pre向后一步
            cur = temp; //cur指向它的下一个节点
            count++;
        }
        int[] res = new int[count];
        int i = 0;
        while(pre != null) {
            res[i++] = (int)pre.val;
            pre = pre.next;
        }
        return res;
    }
}

递归做法:

递归
class Solution {
    ArrayList<Integer> tmp = new ArrayList<Integer>();
    public int[] reversePrint(ListNode head) {
        recur(head);
        int[] res = new int[tmp.size()];
        for(int i = 0; i < res.length; i++)
            res[i] = tmp.get(i);
        return res;
    }
    void recur(ListNode head) {
        if(head == null) return; // 遇到尾节点时终止
        recur(head.next); // 向尾节点的方向一直走
        tmp.add(head.val); // 倒序记录节点
        return; // 因为是“正序开启递归”,所以是回溯的顺序是“倒序”的。
    }
}

作者:jyd
链接:https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/solution/mian-shi-ti-06-cong-wei-dao-tou-da-yin-lian-biao-d/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

辅助栈:

辅助栈做法
class Solution {
    public int[] reversePrint(ListNode head) {
        LinkedList<Integer> stack = new LinkedList<Integer>();
        while(head != null) {
            stack.addLast(head.val);
            head = head.next;
        }
        int[] res = new int[stack.size()];
        for(int i = 0; i < res.length; i++)
            res[i] = stack.removeLast();
        return res;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/solution/mian-shi-ti-06-cong-wei-dao-tou-da-yin-lian-biao-d/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

剑指 Offer 24. 反转链表

秒了

查看代码
class Solution {
    public ListNode reverseList(ListNode head) {  
        ListNode prev = null;   
        ListNode curr = head;   
        while (curr != null) {  
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
}

 

剑指 Offer 35. 复杂链表的复制 

没能看懂题目什么意思。

复制这个链表,并输出。可以借助哈希表。创建一个哈希表,建立“原结点 -> 新创建结点” 的 Map 映射,接着遍历原来的链表,开始构建新链表的next和random的指向。利用map可以找到链表中的所有节点,最后返回哈希表的值结点,map中的键结点对应的值结点构成的链表,是原链表的复制链表。

哈希表
class Solution {
    public Node copyRandomList(Node head) {
        if(head == null) return null;
        Node cur = head;
        Map<Node, Node> map = new HashMap<>();
        // 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
        while(cur != null) {
            map.put(cur, new Node(cur.val));
            cur = cur.next;
        }
        cur = head;
        // 4. 构建新链表的 next 和 random 指向
        while(cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        // 5. 返回新链表的头节点
        return map.get(head);
    }
}

作者:jyd
链接:https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/solution/jian-zhi-offer-35-fu-za-lian-biao-de-fu-zhi-ha-xi-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

 

拼接+拆分
class Solution {
    public Node copyRandomList(Node head) {
        if(head == null) return null;
        Node cur = head;
        // 1. 复制各节点,并构建拼接链表
        while(cur != null) {
            Node tmp = new Node(cur.val);
            tmp.next = cur.next;
            cur.next = tmp;
            cur = tmp.next;
        }
        // 2. 构建各新节点的 random 指向
        cur = head;
        while(cur != null) {
            if(cur.random != null)
                cur.next.random = cur.random.next;
            cur = cur.next.next;
        }
        // 3. 拆分两链表
        cur = head.next;
        Node pre = head, res = head.next;
        while(cur.next != null) {
            pre.next = pre.next.next;
            cur.next = cur.next.next;
            pre = pre.next;
            cur = cur.next;
        }
        pre.next = null; // 需要保持原链表不变
        return res;      // 返回新链表头节点
    }
}

作者:jyd
链接:https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/solution/jian-zhi-offer-35-fu-za-lian-biao-de-fu-zhi-ha-xi-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

字符串(简单)

剑指 Offer 05. 替换空格

Java 语言中,字符串都被设计成「不可变」的类型,即无法直接修改字符串的某一位字符,需要新建一个字符串实现。新建的 StringBuilder,将string变成字符串数组,将每个字符添加到stringBuilder对象中;

查看代码

class Solution {
    public String replaceSpace(String s) {
        StringBuilder res = new StringBuilder();
        for(Character c : s.toCharArray())
        {
            if(c == ' ') res.append("%20");
            else res.append(c);
        }
        return res.toString();
    }
}

作者:jyd
链接:https://leetcode.cn/problems/ti-huan-kong-ge-lcof/solution/mian-shi-ti-05-ti-huan-kong-ge-ji-jian-qing-xi-tu-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

剑指 Offer 58 - II. 左旋转字符串

定位字符串中的某个字符s.charAt(index )。从n下标开始遍历并将字符添加到StringBuilder对象中。之后将0-n下标元素添加。

查看代码

class Solution {
    public String reverseLeftWords(String s, int n) {
        StringBuilder res = new StringBuilder();
        for(int i = n; i < s.length(); i++)
            res.append(s.charAt(i));
        for(int i = 0; i < n; i++)
            res.append(s.charAt(i));
        return res.toString();
    }
}

作者:jyd
链接:https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/solution/mian-shi-ti-58-ii-zuo-xuan-zhuan-zi-fu-chuan-qie-p/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

查找算法

剑指 Offer 03. 数组中重复的数字

较为简单的做法是使用辅助数组,如果能想到容器,可以使用hashset也可以。

辅助数组
class Solution {
    public int findRepeatNumber(int[] nums) {
        //use array
        int[] arr = new int[nums.length];
        //给辅助数组中赋值;
        for(int i = 0; i < nums.length; i++){
            arr[nums[i]]++;
        }
        //检查辅助数组
        for(int i = 0; i < arr.length; i++){
            if(arr[i] > 1){
                return i;
            }
        }
        return -1;
    }
}
HashSet做法
class Solution {
    public int findRepeatNumber(int[] nums) {
        Set<Integer> dic = new HashSet<>();
        for(int num : nums) {
            if(dic.contains(num)) return num;
            dic.add(num);
        }
        return -1;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/solution/mian-shi-ti-03-shu-zu-zhong-zhong-fu-de-shu-zi-yua/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原地交换
class Solution {
    public int duplicateInArray(int[] nums) {
         int n = nums.length;
         for(int num:nums){
             if(num <0 || num>=n) return -1;
         }
       for(int i = 0;i<n;i++){
           //长度为n,且数字都在 0∼n−1 的范围内。数组元素的 索引 和 值 是 一对多 的关系
           //如果相等,说明当前数字已在对应索引位置,跳过。
           //不相等说明nums[i]没有放在正确的位置。
           while(nums[i] != i){
               //nums[i]没有放在正确的位置,并且要放的位置已经有相同的数字,返回重复数字。
               if(nums[nums[i]] == nums[i]) return nums[i];
               //nums[i]没有放在正确的位置,并且要放的位置和这个位置也不匹配。
               int temp = nums[i];
               nums[i] = nums[nums[i]];
               nums[temp] = temp;//注意交换最后一句。不然会超时。如果Java另外调用一个交换函数,把值传入,还是会超时,因为传入的值是值传递,不会修改内存中指针的指向。
           }
       }
       return -1;
    }
}

时间复杂度 O(N)
空间复杂度 O(1)

 

剑指 Offer 53 - I. 在排序数组中查找数字 I

如果直接遍历并计数的解法太没意思了。其实在二分查找的基础上改进了一下,找到不和target相等的两个边界:左边界和右侧边界。

 

二分查找的时候使用if(nums[mid] <= target) i=mid+1; 找右边界,最后的到底的右边界,在目标值右侧下标位置。

用二分找边界
class Solution {
    public int search(int[] nums, int target) {
        // 搜索右边界 right
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] <= target) i = m + 1; //目标值较大,继续向右侧区间偏移,最终i下标左侧元素是target。
            else j = m - 1;
        }
        int right = i;
        // 若数组中无 target ,则提前返回
        if(j >= 0 && nums[j] != target) return 0;
        // 搜索左边界 right
        i = 0; j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] >= target) j = m - 1;//目标值较小,继续向左侧区间偏移,最终j下标右侧元素位是target。
            else i = m + 1;
        }
        int left = j;
        return right - left - 1;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/solution/mian-shi-ti-53-i-zai-pai-xu-shu-zu-zhong-cha-zha-5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 53 - II. 0~n-1中缺失的数字

不太通用的方法:遍历实现,需要考虑特殊情况。或者新建一个数组实现空间复杂度o(n)的做法。

查看代码

class Solution {
    public int missingNumber(int[] nums) {
        int len = nums.length;
        if(nums == null || 0 == len) return 0;

        if(nums[len-1] != len) return len; // 末尾缺数字。0,1,2的情况。
        
        for(int i = 0; i < len; i++){ //中间缺少数字
            if(nums[i] != i){
                return i;
            }
        }
        return -1;
    }
}

题解:(二分) O(logn)

二分查找:

“排序数组中的搜索问题,首先想到 二分法 解决”。

二分法,从i,j开始二分,最后 j<i ,j是左子数组的最后下标,i是右子数组的第一个下标,也就是确实的数组元素。

二分做法
class Solution {
    public int missingNumber(int[] nums) {
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] == m) i = m + 1; //说明左子数组不缺元素,右子数组缺少元素
            else j = m - 1;//左子数组缺元素
        }
        return i;
    }
}
作者:jyd
链接:https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/solution/mian-shi-ti-53-ii-0n-1zhong-que-shi-de-shu-zi-er-f/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

由于数组中含有重复元素,最坏时间复杂度是O(n)

前半段和后半段都是单调上升的,有可能开始和末尾是相同的。先把末尾相同的部分干掉,就可以进行二分

class Solution {
    public int findMin(int[] nums) {
        int n=nums.length - 1;//下标
        if(n<0)return -1;
        //去掉数组后面可能的重复元素
        while(n>0 && nums[n] == nums[0]) n--;
        //可能后面部分不存在,直接返回nums[0]
        if(nums[n] >= nums[0]) return nums[0];
        //进行二分查找
        int l=0,r=n;
        while(l < r){
            int mid = l+r >> 1;
            //中间的元素小,说明最小的元素可能再中间元素的左侧
            if(nums[mid] < nums[0]) r = mid;
            //中间元素大于开头元素,说明最小元素再中间元素右侧
            else l = mid + 1;
        }
        return nums[r];
    }
}

另一种思路:

思路:使用二分查找思路,中间位置下标值和右侧位置下标值做对比,如果nums[m] > nums[j]时,说明旋转点在右侧。如果nums[m] < nums[j] 说明旋转点在左侧,下标变更为j=m,比如:3,1,3测试用例。如果相等,j下标减一再判断。

二分思路
class Solution {
    public int minArray(int[] numbers) {
        int i = 0, j = numbers.length - 1;
        while (i < j) {
            int m = (i + j) / 2;
            if (numbers[m] > numbers[j]) i = m + 1;// demo:23412  ,4<2
            else if (numbers[m] < numbers[j]) j = m;//demo:51234  ,2<4
            else j--;
        }
        return numbers[i];
    }
}

作者:jyd
链接:https://leetcode.cn/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/solution/mian-shi-ti-11-xuan-zhuan-shu-zu-de-zui-xiao-shu-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 剑指 Offer 50. 第一个只出现一次的字符

思路:原想定义一个map,键存储字符,值存储字符出现的次数,题目让找到是第一个出现一次的,这样只需要将值设置成为布尔值,如果已经包含该值就改变布尔值,之后再次遍历哈希表元素,找到第一个符合条件的字符,并返回。

map.getKeySet(),或者map.entrySet()会乱序排列key值。

哈希表

class Solution {
    public char firstUniqChar(String s) {


        Map<Character,Boolean> map = new HashMap<>();
        char[] res = s.toCharArray();
        for(char c : res){
            if(map.containsKey(c)) map.put(c,false);
            else map.put(c,true);
        }
        for(char c : res){
            if(map.get(c) == true) return c;
        }
        return ' ';
    }
}

剑指 Offer 04. 二维数组中的查找

最坏时间复杂度是O(n^2),这道题有线性时间复杂度o(n+m)的做法。

例如:每次判断目标值和右上角数字的关系。目标值比右上角数字小,那么目标值就在右上角数字左侧的范围内。那么这一列都不用判断了,因为这一列都比当前数字大。也可以以左下角元素为基准进行判断。

整体思路:

以左下角元素为主,如果比目标元素大,说明当前行都比目标元素大,消去行,i--。

如果比目标元素小,说明当前列都比目标元素小,消去列。如果等于目标元素返回true。进行遍历的条件是i下标大于等于0,j下标小于列长;

查看代码
class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        int i = matrix.length - 1,j = 0;
        while(i >= 0 && j < matrix[0].length){ // &&短路与特性,不能调换位置。如果调换位置,[]会报错:matrix[0]越界。
            if(target < matrix[i][j]) i--;
            else if(target > matrix[i][j]) j++;
            else return true;
        }
        return false;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/solution/mian-shi-ti-04-er-wei-shu-zu-zhong-de-cha-zhao-zuo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

右上角元素为基准,需要判断数组为空(为[])的情况。

https://juejin.cn/post/6844903928820318222

 

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        //if((matrix==null || matrix.length == 0) || (matrix.length==1 && matrix[0].length==0)){return false;}
        if(matrix.length == 0 || matrix[0].length == 0) return false;//对[]做判断
        int i = 0;
        int j = matrix[0].length - 1;
        while(i < matrix.length && j >= 0) {
            if(matrix[i][j] > target) j--;
            else if(matrix[i][j] < target) i++;
            else return true;
        }
        return false;
    }
}

搜索与回溯算法

剑指 Offer 32 - I. 从上到下打印二叉树

宽度优先遍历,使用队列辅助遍历。

注:LinkedLlist用来定义队列。https://blog.csdn.net/jjruanlili/article/details/112958952

步骤:

1,特例处理: 当树的根节点为空,直接返回空数组

2,初始化:一个链表,一个队列

3,BFS 循环: 当队列 queue 为空时跳出;

  出队: 队首元素出队,记为 node;
  打印: 将 node.val 添加至列表 tmp 尾部;
  添加子节点: 若 node 的左(右)子节点不为空,则将左(右)子节点加入队列 queue ;

4,返回结果数组

查看代码
class Solution {
    public int[] levelOrder(TreeNode root) {
        if(root == null) return new int []{};
        List<Integer> list = new ArrayList<>();
        Queue<TreeNode> q = new LinkedList<>(){{offer(root);}};
        while(!q.isEmpty()){
            TreeNode node = q.poll();
            list.add(node.val);
            if(node.left != null) q.offer(node.left);
            if(node.right != null) q.offer(node.right);
        }
        int[] res = new int[list.size()];
        for(int i = 0; i < list.size(); i++){
            res[i] = list.get(i);
        }
        return res;
    }
}

 

剑指 Offer 32 - II. 从上到下打印二叉树 II

广度优先遍历,先做特例处理。将根节点入队列。如果队列不为空则执行循环:

定义一个链表,存储每层节点,并将当前 的队列元素里面个数记为count,也就是每层节点的数量,如果count大于0,执行循环操作:

当前队头元素出栈记为node,加入该层的链表中,node的左右子树如果不为空,也加入队列,count减一。

循环执行完,二叉树的这一层元素被加入list中。

用队列广度优先遍历
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if(root == null) return new ArrayList();
        Queue<TreeNode> q = new LinkedList<TreeNode>(){{offer(root);}};
        List tar = new ArrayList();
        while(!q.isEmpty()){
            List<Integer> list = new ArrayList<Integer>();//存储每层节点的list
            int count = q.size();//每层的节点数量
            
            while(count > 0){
                TreeNode node = q.poll();
                list.add(node.val);
                if(node.left != null) q.offer(node.left);
                if(node.right != null) q.offer(node.right);
                count--;
            }
            tar.add(list);//将每层节点放入总的list里面
        }
        return tar;
    }
}

剑指 Offer 32 - III. 从上到下打印二叉树 III

先按照上提做法,最后做判断如果目标list中已经有奇数个元素,将这次循环得到的元素逆序,在加入list中。另外还可以使用双端队列做法。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if(root == null) return new ArrayList();
        Queue<TreeNode> q = new LinkedList(){{offer(root);}};
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        

        while(!q.isEmpty()){
            List<Integer> list = new ArrayList();

            int count  = q.size();
            while(count > 0){
                TreeNode node = q.poll();
                list.add(node.val);
                if(node.left != null) q.offer(node.left);
                if(node.right != null) q.offer(node.right);
                count--;
            }
            if(res.size() % 2 == 1) Collections.reverse(list);//开始size()结果是0
            res.add(list);
        }
        return res;
    }
}

剑指 Offer 26. 树的子结构

1,先序遍历树 A 中的每个节点Na,函数isSubStructure(A, B)。

2,判断树 A 中 以 Na 为根节点的子树 是否包含树B。

recur(A, B),递归判断第二棵树B的节点是否在第一棵树A中都有对应的点。

class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {//树 B 是 树 A的子结构
        if(A == null || B == null) return false;
        if(recur(A,B) return true;
        return isSubStructure(A.left, B) || isSubStructure(A.right, B);
        //return (A != null && B != null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
    }
    boolean recur(TreeNode A, TreeNode B) {
        if(B == null) return true;//节点B为空:说明树B已匹配完成(越过叶子节点)
        if(A == null || A.val != B.val) return false;//当节点A为空:说明已经越过树 A叶子节点,即匹配失败
        return recur(A.left, B.left) && recur(A.right, B.right);//如果A,B根节点 同时找左子树和右子树。
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/solution/mian-shi-ti-26-shu-de-zi-jie-gou-xian-xu-bian-li-p/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 27. 二叉树的镜像

递归做法:

public TreeNode mirrorTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        TreeNode leftRoot = mirrorTree(root.right);
        TreeNode rightRoot = mirrorTree(root.left);
        root.left = leftRoot;
        root.right = rightRoot;
        return root;
    }

辅助栈做法:

class Solution {
	public TreeNode mirrorTree(TreeNode root) {
		if(root == null) return null;
		Queue<TreeNode> queue = new LinkedList<>(){{offer(root);}};
		while(!queue.isEmpty()) {
			TreeNode node = queue.poll();

			TreeNode tmp = node.left;
			node.left = node.right;
			node.right = tmp;

			if(node.left != null) queue.offer(node.left);
			if(node.right != null) queue.offer(node.right);
		}
		return root;
	}
}

剑指 Offer 28. 对称的二叉树

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        return dfs(root.left,root.right);
    }
    public boolean dfs(TreeNode left,TreeNode right) {
        if(left == null && right == null) return true;
        //两个都为空,其中一个为空,都不为空,三种情况。
        if(left == null || right == null) return false;

        if(left.val != right.val) return false;
        return dfs(left.left,right.right) && dfs(left.right,right.left);
    }
}

动态规划(简单)

剑指 Offer 10- I. 斐波那契数列

求解斐波那契数列的若干方法 - AcWing,优化解法可以延伸到其他题目。

Java动态规划

class Solution {
    public int fib(int n) {
        if(n == 0) return 0;//n从1开始,也保证dp[1]不会数组越界。
        //斐波那契数列第 i 个数字 
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i-1] + dp[i-2];
            dp[i] %= 1000000007;
        }
        return dp[n];
    }
}

对动态规划进行空间复杂度优化:

class Solution {
    public int fib(int n) {
        //对应:
        // a , b , sum ..
        // . , a , b , ... 
        //0,1,1,2,3,5......
        int a = 0, b = 1, sum;
        for(int i = 0; i < n; i++){//进行n次循环
            sum = (a + b) % 1000000007;//大数越界
            a = b;
            b = sum;
        }
        return a;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/solution/mian-shi-ti-10-i-fei-bo-na-qi-shu-lie-dong-tai-gui/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 10- II. 青蛙跳台阶问题

动态规划写法

动态规划

class Solution {
    public int numWays(int n) {
        if(n == 1) return 1;
        int[] dp = new int[n+1];
        dp[0] = 1;
        if(n > 1) dp[1] = 1;
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i - 1] + dp[i - 2];
            dp[i] %= 1000000007;
        }
        return dp[n];
    }
}

 

剑指 Offer 63. 股票的最大利润

DP思想,思路:

  1. 记录【今天之前买入的最小值】
  2. 计算【今天之前最小值买入,今天卖出的获利】,也即【今天卖出的最大获利】
  3. 比较【每天的最大获利】,取最大值即可
查看代码
class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1) return 0;
        int min = prices[0],max = 0;
        for(int i= 1; i < prices.length; i++){
            //第i天的最大收益
            max = Math.max(prices[i] - min,max);
            //第i天之前的最小股价
            min = Math.min(min,prices[i]);
        }
        return max;
    }
}

动态规划(中等)

剑指 Offer 42. 连续子数组的最大和

大概思路

动态规划解法:

动态规划未优化
public class Solution {

    public int maxSubArray(int[] nums) {
        int len = nums.length;
        // dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
        int[] dp = new int[len];
        dp[0] = nums[0];

        for (int i = 1; i < len; i++) {
            if (dp[i - 1] > 0) {
                dp[i] = dp[i - 1] + nums[i];
            } else {
                dp[i] = nums[i];
            }
        }

        // 也可以在上面遍历的同时求出 res 的最大值,这里我们为了语义清晰分开写,大家可以自行选择
        int res = dp[0];
        for (int i = 1; i < len; i++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

作者:liweiwei1419
链接:https://leetcode.cn/problems/maximum-subarray/solution/dong-tai-gui-hua-fen-zhi-fa-python-dai-ma-java-dai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

动态规划优化版本
public class Solution {

    public int maxSubArray(int[] nums) {
        int pre = 0;
        int res = nums[0];
        for (int num : nums) {
            pre = Math.max(pre + num, num);
            res = Math.max(res, pre);
        }
        return res;
    }
}

作者:liweiwei1419
链接:https://leetcode.cn/problems/maximum-subarray/solution/dong-tai-gui-hua-fen-zhi-fa-python-dai-ma-java-dai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 47. 礼物的最大价值

 

查看代码

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(i == 0 && j == 0) continue;
                if(i == 0) grid[i][j] += grid[i][j - 1] ;
                else if(j == 0) grid[i][j] += grid[i - 1][j];
                else grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
            }
        }
        return grid[m - 1][n - 1];
    }
}

作者:jyd
链接:https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/solution/mian-shi-ti-47-li-wu-de-zui-da-jie-zhi-dong-tai-gu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

或者 多开一行一列,更简洁一些:

查看代码

class Solution {
    public int maxValue(int[][] grid) {
        int row = grid.length;
        int column = grid[0].length;
        //dp[i][j]表示从grid[0][0]到grid[i - 1][j - 1]时的最大价值
        int[][] dp = new int[row + 1][column + 1];
        for (int i = 1; i <= row; i++) {
            for (int j = 1; j <= column; j++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
            }
        }
        return dp[row][column];
    }
}

动态规划(中等)

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

dp[0] = dp[1] = 1;即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1

动态规划未优化
class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int len = s.length();
        int[] dp = new int[len+1];
        dp[0] = 1;
        dp[1] = 1;
        for(int i =2 ; i < len+1; i++){
            String tmp = s.substring(i-2,i);
            if(tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0){
                dp[i] = dp[i - 1] + dp[i - 2];
            }else{
                dp[i] = dp[i - 1];
            }
        }
        return dp[len];//dp[i]代表以i为下标的数字的翻译方案的数量
    }
}

 

动态规划优化过的代码
class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int a = 1, b = 1;
        for(int i = 2; i <= s.length(); i++) {
            String tmp = s.substring(i - 2, i);
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/solution/mian-shi-ti-46-ba-shu-zi-fan-yi-cheng-zi-fu-chua-6/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

动态规划,js版本

const translateNum = (num) => {
  const str = num.toString();
  const n = str.length;

  const dp = new Array(n + 1);
  dp[0] = 1;
  dp[1] = 1;

  for (let i = 2; i < n + 1; i++) {
    const temp = Number(str[i - 2] + str[i - 1]);
    if (temp >= 10 && temp <= 25) {
      dp[i] = dp[i - 1] + dp[i - 2];
    } else {
      dp[i] = dp[i - 1];
    }
  }
  
  return dp[n]; // 翻译前n个数的方法数,即翻译整个数字
}

作者:xiao_ben_zhu
链接:https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/solution/shou-hui-tu-jie-dfsdi-gui-ji-yi-hua-di-gui-dong-ta/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

剑指 Offer 48. 最长不含重复字符的子字符串

好难

双指针

题解

双指针
 class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res = 0;
        Set<Character> set = new HashSet<>();
        for(int l = 0, r = 0; r < s.length(); r++) {
            char c = s.charAt(r);
            while(set.contains(c)) {
                set.remove(s.charAt(l++));
            }
            set.add(c);
            res = Math.max(res,r - l + 1);
        }
        return res;
    }
}
滑动窗口做法
public int lengthOfLongestSubstring(String s) {
        //if(s==null) return 0;这句话可以不加,s.length()长度为0时,不进入下面的循环,会直接返回max=0;
        //划定当前窗口的坐标为(start,i],左开右闭,所以start的初始值为-1,而非0。
        int max = 0,start =-1;
        //使用哈希表map统计各字符最后一次出现的索引位置
        HashMap<Character,Integer> map = new HashMap<>();
        for(int i=0;i<s.length();i++){
            char tmp = s.charAt(i);
            
            //当字符在map中已经存储时,需要判断其已经存储的索引值index和当前窗口start的大小以确定是否需要对start进行更新:
            //当已存储的index > start时,start更新为当前的index,即更新窗口的左边界,否则保持不变。
            //注意若index作为新的start,计算当前滑动空间的长度时也是不计入的,左开右闭,右侧s[i]会计入,这样也是防止字符的重复计入。
            //带入字符串:pwwkew
            if(map.containsKey(tmp)) start = Math.max(start,map.get(tmp));
            
            //Math.max(start,map.get(tmp);可以保证测试用例:abba
            
            //如果map中不含tmp,此处是在map中新增一个键值对,否则是对原有的键值对进行更新
            map.put(tmp,i);
            
            //i-start,为当前滑动空间(start,i]的长度,若其大于max,则需要进行更新。
            max = Math.max(max,i-start);
        }
        return max;
    }

双指针(简单)

剑指 Offer 18. 删除链表的节点

双指针

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(head.val == val) return head.next;
        ListNode pre = head, cur = head.next;
        while(cur != null && cur.val != val) {
            pre = cur;
            cur = cur.next;
        }
        if(cur != null) pre.next = cur.next;
        return head;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/solution/mian-shi-ti-18-shan-chu-lian-biao-de-jie-dian-sh-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
单指针操作

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(val == head.val){
            return head.next;
        }
        ListNode cur = head;
        while(cur.next != null && cur.next.val != val){
            cur = cur.next;
        }
        if(cur.next != null){
                cur.next = cur.next.next;
            }
        return head;
    }
}

剑指 Offer 22. 链表中倒数第k个节点

双指针法

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode pre = head,cur = head;
        for(int i = 0; i < k; i++){
            cur = cur.next;
        }
        while(cur != null){
            cur = cur.next;
            pre = pre.next;
        }
        return pre;
    }
}

剑指 Offer 25. 合并两个排序的链表

使用递归实现,如果L2的第一个元素小,将L2的next指针指向递归,如果L1的元素小较小,继续递归。

查看代码
class Solution {
    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;
        }

    }
}

作者:z1m
链接:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/yi-kan-jiu-hui-yi-xie-jiu-fei-xiang-jie-di-gui-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
双指针迭代

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dum = new ListNode(0), cur = dum;
        while(l1 != null && l2 != null) {
            if(l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            }
            else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = l1 != null ? l1 : l2;
        return dum.next;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/solution/mian-shi-ti-25-he-bing-liang-ge-pai-xu-de-lian-b-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 52. 两个链表的第一个公共节点

这题也是计算机综合考试考研题目真题。

双指针思路:

pA走过的路径为A链表+B链表,pB走过的路径为B链表+A链表

pA和pB走过的长度都相同,都是A链和B链的长度之和,相当于将两条链从尾端对齐,如果相交,则会提前在相交点相遇,如果没有相交点,则会在最后相遇。

pA:1->2->3->4->5->6->null->9->5->6->null
pB:9->5->6->null->1->2->3->4->5->6->null

上面pA和pB的公共节点是5,6

也可理解为:

若相交,链表A: a+c, 链表B : b+c  ,a+c+b+c = b+c+a+c 则会在公共处c起点相遇。

若不相交,a +b = b+a 。因此相遇处是NULL。

双指针题解
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode pA = headA, pB = headB;
    while (pA != pB) {
        pA = pA == null ? headB : pA.next;
        pB = pB == null ? headA : pB.next;
    }
    return pA;
}

作者:reals
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

我的写法

class Solution {
    public int[] exchange(int[] nums) {
        int i = 0,j = nums.length - 1;
        while(i < j){
            if(nums[i] % 2 == 1 ){
                i++;
            }
            else if(nums[j] % 2 == 0){
                j--;
            }
            //swap(i,j);
            int tmp;
            tmp = nums[j];
            nums[j] = nums[i];
            nums[i] = tmp;
        }
        return nums;
    }
}

可以通过,但是当输入全是奇数或者全部偶数,也会改变数组。

参考答案:内层外层都要判定i<j 的情况

查看代码
class Solution {
    public int[] exchange(int[] nums) {
        int i = 0, j = nums.length - 1, tmp;
        while(i < j) {
            //a & 1 == 0 表示a是偶数。最后一个bit是0.
            while(i < j && (nums[i] & 1) == 1) i++;
            while(i < j && (nums[j] & 1) == 0) j--;
            tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
        }
        return nums;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/solution/mian-shi-ti-21-diao-zheng-shu-zu-shun-xu-shi-qi-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 57. 和为s的两个数字

利用有序性,可以使用双指针。

查看代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int i = 0, j = nums.length - 1;
        while(i < j) {
            int s = nums[i] + nums[j];
            if(s < target) i++;//数组中元素和偏小
            else if(s > target) j--;
            else return new int[] { nums[i], nums[j] };
        }
        return new int[0];
    }
}

作者:jyd
链接:https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/solution/mian-shi-ti-57-he-wei-s-de-liang-ge-shu-zi-shuang-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 58 - I. 翻转单词顺序

 

双指针做法
class Solution {
    public String reverseWords(String s) {
        s = s.trim(); // 删除首尾空格
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while(i >= 0) {
            while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
            res.append(s.substring(i + 1, j + 1) + " "); // 添加单词,substring两个下标含头部不含尾
            while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
            j = i; // j 指向下个单词的尾字符
        }
        return res.toString().trim(); // 转化为字符串并返回
    }
}

作者:jyd
链接:https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/solution/mian-shi-ti-58-i-fan-zhuan-dan-ci-shun-xu-shuang-z/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

搜索与回溯算法(中等)

剑指 Offer 12. 矩阵中的路径

acwing的C++代码,带注释:AcWing 23. 矩阵中的路径 - AcWing

主要思路:dfs暴力搜索+回溯

查看代码
class Solution {
    public boolean exist(char[][] board, String word) {
        for(int i = 0; i < board.length; i++){
            for(int j = 0; j < board[0].length; j++){
                if(dfs(board,word,0,i,j)) return true;
            }
        }
        return false;
    }
    /**
    深度遍历作用:根据当前如的字符串枚举路径
    参数:矩阵,当前字符串,当前字符的下标,矩阵中的坐标
    //判断word字符串中index下标的每个相邻字符,是不是符合预期。
     */
    boolean dfs(char[][] board, String word,int index, int x, int y){
        //假设特例[["a"]] "a"
        if(board[x][y] != word.charAt(index)) return false;
        if(index + 1 == word.length()) return true;
        char c = board[x][y];
        board[x][y] = '*';

        //dx:行,dy:列  的变化
        int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
        for(int i = 0; i < 4; i++){
            //变化后的下标
            int a = x + dx[i]; int b = y + dy[i];
            //变化后的下标合法判断
            if(a >= 0 && a < board.length && b >=0 && b < board[0].length){
                //此处不能直接返回dfs的结果,直接return,会连同false一同返回。只能返回是true的结果。
                //如果false也返回结果,会直接导致当前的dfs方法栈返回。可能当前矩阵中字符其他方向的字符串还没有判断完,四个方向中,可能后面才是匹配的字符。
                if(dfs(board,word,index + 1,a,b)) return true;
            }
        }
        board[x][y] = c;//回溯,恢复现场
        return false;
    }
}

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

可以使用深度有限和广度优先遍历。

深度优先dfs:

dfs
public static int movingCount(int m, int n,int k){
        boolean[][] visited = new boolean[m][n];
        int ans = dfs(0,0,m,n,k,visited);
        return ans;
        
    }
    //深度优先实现
    public int dfs(int i,int j,int m,int n,int k,boolean[][] visited){
        if(i<0||i>=m||j<0||j>=n||bitSum(i)+bitSum(j)>k||visited[i][j]==true){//i,j索引超出边界或者数位之和大于k,或者已经访问过了,则返回0,代表不计入可达解
            return 0;
        }
        visited[i][j] = true;
        return 1 + dfs(i-1,j,m,n,k,visited) + dfs(i+1,j,m,n,k,visited) + dfs(i,j-1,m,n,k,visited) + dfs(i,j+1,m,n,k,visited) ;
    }
    
    //数位之和
    public int bitSum(int i){
        int sum = 0;
        do{
            sum+=i%10;
            i = i/10;
        }while(i!=0);
        return sum;
    }

广度优先遍历bfs:

通常利用队列实现广度优先遍历。cpp版本:https://www.acwing.com/activity/content/code/content/1214658/ 

bfs
class Solution {
    class Node{
    int x;
    int y;
    public Node(int x,int y){
        this.x=x;
        this.y=y;
    }
    }
    int get_single_sum(int num){//计算数位和
        int sum = 0;
        while(num > 0){
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
    int get_sum (Node node){//横纵坐标数位和
        return get_single_sum(node.x) + get_single_sum(node.y);
    }
    //x,y轴 对应 上右下左方向的偏移量。
    int dx[] = {-1,0,1,0};int dy[] = {0,1,0,-1};
    public int movingCount(int m, int n, int k) {
        int res = 0;
        boolean[][] visited = new boolean[m][n];
        for(int i = 0;i < m; ++i){
            for(int j = 0; j < n; ++j){
                visited[i][j] = false;
            }
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(new Node(0,0));
        while(!queue.isEmpty()){
            Node node = queue.poll();
            if(get_sum(node) > k || visited[node.x][node.y] == true) continue;
            res++;
            visited[node.x][node.y] = true;
            //遍历四个方向的下标,并将符合条件的下标入栈
            for(int i = 0; i < 4; i++){
                int x = node.x + dx[i],y = node.y + dy[i];
                if(x >= 0 && x < m && y >= 0 && y < n){
                    queue.offer(new Node(x,y));
                }
            }
        }
        return res;
    }
}

day15 搜索与回溯算法(中等)

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

先序遍历+路径记录 

记录路径时若直接执行 res.append(path) ,则是将 path 对象加入了 res ;后续 path 改变时, res 中的 path 对象也会随之改变。

正确做法:res.append(list(path)) ,相当于复制了一个 path 并加入到 res 。

//LinkedList构造函数
public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
查看代码
class Solution {
    LinkedList<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>(); 
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        recur(root, sum);
        return res;
    }
    void recur(TreeNode root, int tar) {
        if(root == null) return;
        path.add(root.val);
        tar -= root.val;
        if(tar == 0 && root.left == null && root.right == null)
            res.add(new LinkedList(path));
        recur(root.left, tar);
        recur(root.right, tar);
        
        ////向上回溯前,需要将当前节点从路径 path 中删除,即执行 path.pop()
        /// demo:到叶节点,且不满足条件,返回递归的上一级,要将path列表中的组后一个元素删除
        path.removeLast();
    }
}

作者:jyd
链接:https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/solution/mian-shi-ti-34-er-cha-shu-zhong-he-wei-mou-yi-zh-5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

剑指 Offer 36. 二叉搜索树与双向链表

视频题解:https://www.bilibili.com/medialist/play/watchlater/BV1jv411n7ru

利用二叉搜索树中序遍历,从小到大排列的性质。

 

/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
    Node pre,head;

    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        inOrder(root);

        head.left = pre;
        pre.right = head;

        return head;
    }
    public void inOrder(Node cur){
        //dfs优先考虑收敛条件 
        if(cur == null) return;
        inOrder(cur.left);

        if(pre == null){
            head = cur;
        }else{
            //pre 作为中间的结点
            pre.right = cur;
            cur.left = pre;
        }
        pre = cur;
        inOrder(cur.right);
        
    }
}

pre作为中间节点:

 

明白递归调用中cur是在变化的,体会递归变化过程中节点变化。

首先递归一直走到最左边节点,1,该节点左子树为空返回,然后,初始化pre设置为1节点,访问右子树为空返回。

接下来回到父节点2的递归栈节点,cur现在指向节点2,pre现在指向节点1,将pre右子树指向cur,cur的左子树指向pre,pre指向cur。再递归cur的右子树。

进入节点3的递归栈节点,cur现在指向节点3,pre指向节点2,接着建立节点2和节点3之间的指向(将pre右子树指向cur,cur的左子树指向pre)。pre指向当前的cur节点3。

退出节点3和节点2的递归栈,返回到节点4,现在cur指向节点4,pre现在指向节点3。建立节点4和3之间指向。

最后,pre节点指向最后一个节点,head指向第一个节点,建立头尾的指针关联。

 

 

剑指 Offer 54. 二叉搜索树的第k大节点

class Solution {
    int res, k;
    public int kthLargest(TreeNode root, int k) {
        this.k = k;
        dfs(root);
        return res;
    }
    void dfs(TreeNode root) {
        if(root == null) return;
        dfs(root.right);
        if(k == 0) return;
        if(--k == 0) res = root.val;
        dfs(root.left);
    }
}

dfs中第一个if判断中,最优的剪枝判断条件应该是 if(root==null||count<=0) return; 写成==0的的话,递归到右节点结束的情况,会再运行一次--count才进入 dfs(root.left); , 这样一来conunt会直接==-1,没有剪枝效果。

   3
  / \
 1   4
  \
   2

比如先找到节点4之后,count变为0,此时递归栈里面root是节点3,刚执行过dfs(root.right),然后执行--count后count变为-1,需要执行dfs(root.left),这时候需要进入节点1的递归栈,但是count<=0,直接return。

int count=0, res=0;//形参k不能随着dfs的迭代而不断变化,为了记录迭代进程和结果,引入类变量count和res。
    public int kthLargest(TreeNode root, int k) {
        this.count=k;//利用形参值k对类变量count进行初始化
        dfs(root);//这里不要引入形参k,dfs中直接使用的是初始值为k的类变量count
        return res;            
    }
    public void dfs(TreeNode root){
        if(root==null || count <= 0) return;//当root为空或者已经找到了res时,直接返回
        dfs(root.right);
        if(--count==0){//先--,再判断
            res = root.val;
            return;//这里的return可以避免之后的无效迭代dfs(root.left);
        }
        dfs(root.left);  
    }
//https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/solution/mian-shi-ti-54-er-cha-sou-suo-shu-de-di-k-da-jie-d/
//评论答案@莫言

排序(简单)

剑指 Offer 45. 把数组排成最小的数

先看一个快排模板:

查看代码
public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for (int i = 0; i < nums.length; i++) {
            strs[i] = String.valueOf(nums[i]);
        }
        quickSort(strs, 0, strs.length - 1);
        StringBuilder res = new StringBuilder();
        for (String s : strs)
            res.append(s);
        return res.toString();
    }

    public void quickSort(String[] strs, int low, int high) {
        if (low < high) {
            int middle = getMiddle(strs, low, high);
            quickSort(strs, low, middle - 1);
            quickSort(strs, middle + 1, high);
        }
    }

    public int getMiddle(String[] strs, int low, int high) {
        //数组的第一个数为基准元素
        String temp = strs[low];
        while (low < high) {
            //从后向前找比基准小的数
            while (low < high && (strs[high] + temp).compareTo(temp + strs[high]) >= 0)
                high--;
            //把比基准小的数移到低端
            strs[low] = strs[high];
            //从前向后找比基准大的数
            while (low < high && (strs[low] + temp).compareTo(temp + strs[low]) <= 0)
                low++;
            //把比基准大的数移到高端
            strs[high] = strs[low];
        }
        strs[low] = temp;
        return low;
    }

其中,Java中字符串大小(字典序)对比的实现方法:A.compareTo(B);

规定“序判断规则”为:

若拼接字符串 x + y > y + x ,则 x “大于” y ;
反之,若 x + y < y + x ,则 x “小于” y ;

思路:

初始化字符串,将int类型的数组转换成String类型的数组,基于排序规则对字符串进行排序,最后将数组拼接成字符串并返回。

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++)
            strs[i] = String.valueOf(nums[i]);
        quickSort(strs, 0, strs.length - 1);
        StringBuilder res = new StringBuilder();
        for(String s : strs)
            res.append(s);
        return res.toString();
    }
    void quickSort(String[] strs, int l, int r) {
        if(l >= r) return;
        int i = l, j = r;
        String tmp = strs[i];
        while(i < j) {
            //if [j] >= [l] then j--;
            while((strs[j] + strs[l]).compareTo(strs[l] + strs[j]) >= 0 && i < j) j--;
            //if [i] <= [l] then i++;
            while((strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0 && i < j) i++;
            // result:[j] < [l] < [i]
            //then swap [i],[j]
            tmp = strs[i];
            strs[i] = strs[j];
            strs[j] = tmp;
        }
        //i下标的元素,和l下标元素 互换
        // 由于之前i下标元素和j下标元素交换,现在又交换之后j下标 原来的元素到l下标
        strs[i] = strs[l];
        strs[l] = tmp;
        quickSort(strs, l, i - 1);
        quickSort(strs, i + 1, r);
    }
}

作者:jyd
链接:https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/solution/mian-shi-ti-45-ba-shu-zu-pai-cheng-zui-xiao-de-s-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上面使用三向切分的快速排序。

也可以直接调用排序函数,下面是重写比较器的方法实现。

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

判断步骤:

遇到0,可以忽略
判断是否有重复元素,true则不合法,即任意两个数相同则一定不合法。
最后,最小值与最大值是否相差小于n,true则合法。即可以用0(大小王)代替缺失的数。

查看代码
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 则可构成顺子
    }
}

作者:jyd
链接:https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/solution/mian-shi-ti-61-bu-ke-pai-zhong-de-shun-zi-ji-he-se/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 40. 最小的k个数

topK问题,可以利用快速排序,当枢轴元素左侧对应下标是k的时候,说明该元素左侧有K个元素,直接返回左侧的K个元素。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        //传入最后一个参数是下标
        return quickSearch(arr, 0, arr.length - 1, k - 1);
    }
    private int[] quickSearch(int[] nums, int low, int hight, int k){
        int j = partition(nums, low, hight);
        if (j == k) {
            //copyOf中的第二个元素是返回的数组的长度,下表为j,数组长度为j+1;
            return Arrays.copyOf(nums, j + 1);
        }
        return j > k ? quickSearch(nums, low, j - 1, k) : quickSearch(nums, j + 1, hight, k);
    }
    //快排的切分,返回下标j
    private int partition(int[] nums, int low, int hight) {
        int v = nums[low];
        int i = low, j = hight + 1;
        while (true){
            //注意使用++i,和--j
            //i,j初始的值都在边界之外
            while (++i <= hight && nums[i] < v);
            while (--j >= low && nums[j] > v);
            if(i >= j) {
                break;
            }
            int t = nums[j];
            nums[j] = nums[i];
            nums[i] = t;
        }
        nums[low] = nums[j];//到这里i == j?
        nums[j] = v;//将枢轴值放到它的下标
        return j;
    }
}

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

难度:hard

class MedianFinder {
    Queue<Integer> A, B;
    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
    }
    public void addNum(int num) {
        if(A.size() != B.size()) {
            A.add(num);
            B.add(A.poll());
        } else {
            B.add(num);
            A.add(B.poll());
        }
    }
    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/solution/mian-shi-ti-41-shu-ju-liu-zhong-de-zhong-wei-shu-y/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

搜索与回溯算法(中等)

剑指 Offer 55 - I. 二叉树的深度

可以使用dfs,bfs。

dfs的递归实现比较简单。也是后序遍历。

递归实现
 class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        else {
            int left = maxDepth(root.left);
            int right = maxDepth(root.right);
            return Math.max(left, right) + 1;
        }
    }
}

 

剑指 Offer 55 - II. 平衡二叉树

本题定义的平衡二叉树,只须满足左右子树高度差不超过1。

实际AVL树是由BST二叉搜索树优化来的。满足左小右大性质。

自顶向下。先序遍历+判断深度
 class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) return true;
        return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }

    private int depth(TreeNode root) {
        if (root == null) return 0;
        return Math.max(depth(root.left), depth(root.right)) + 1;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/solution/mian-shi-ti-55-ii-ping-heng-er-cha-shu-cong-di-zhi/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 64. 求1+2+…+n

class Solution {
    int res = 0;
    public int sumNums(int n) {
        // n > 1 用作终止递归,,n == 1时候,后面的递归不能执行
        // && 短路与,前面成立就不再对后面进行校验,优先级高与 =
        // sumNums(n - 1) 开启递归
        boolean x = n > 1 && sumNums(n - 1) > 0;
        res += n;
        return res;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/qiu-12n-lcof/solution/mian-shi-ti-64-qiu-1-2-nluo-ji-fu-duan-lu-qing-xi-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

 

若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:

p 和 q 在 root的子树中,且分列 root的 异侧(即分别在左、右子树中);
p = root,且 q 在 root 的左或右子树中;
q = root,且 p 在 root 的左或右子树中;

根据上面的最近公共祖先的情况和二叉树的定义,用迭代的方法。向一个方向去迭代。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while(root != null) {
            if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中
                root = root.right; // 遍历至右子节点
            else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中
                root = root.left; // 遍历至左子节点
            else break;
        }
        return root;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/solution/mian-shi-ti-68-i-er-cha-sou-suo-shu-de-zui-jin-g-7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

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

不是二叉搜索树,解法挺巧妙。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || 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一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
        if(right == null) return left; // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况,如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
        return root; //否则,当 left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root
    }
}

作者:jyd
链接:https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/solution/mian-shi-ti-68-ii-er-cha-shu-de-zui-jin-gong-gon-7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

分治算法(中等)

剑指 Offer 07. 重建二叉树

思路很好理解,可是代码想不到

class Solution {
    int[] preorder;
    HashMap<Integer, Integer> dic = new HashMap<>();//利用无重复元素这一题目条件
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        for(int i = 0; i< inorder.length; i++){
            dic.put(inorder[i], i);
        }
        return recur(0, 0, inorder.length - 1);
    }
    TreeNode recur(int root, int left, int right){
        if(left > right) return null;
        TreeNode node = new TreeNode(preorder[root]);//构建根节点
        int i = dic.get(preorder[root]); //划分根节点,左子树,右子树。得到的i是根节点在中序遍历中的下标
        node.left = recur(root + 1, left, i - 1); 
        node.right = recur(root + i - left + 1, i + 1, right);//右子树的根节点??当前根位置 + 左子树的数量 + 1
        return node;
    }
}

详细注释:

//preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
//preorder作用找到根节点,inorder找左右子树
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;// 相等的话就是自己
        int pre_root_value = preorder[pre_root];//前序遍历根节点的值
        TreeNode root = new TreeNode(pre_root_value);//获取root节点
        int in_root = map.get(pre_root_value);//获取在中序遍历中根节点所在索引,以方便获取左子树的数量
        //左子树的根的索引为先序中的根节点+1 
        //递归左子树的左边界为原来的中序in_left
        //递归左子树的右边界为中序中的根节点索引-1
        root.left = recur(pre_root+1, in_left, in_root-1);
        //右子树的根的索引为先序中的 当前根位置 + 左子树的数量 + 1
        //递归右子树的左边界为中序中当前根节点+1
        //递归右子树的右边界为中序中原来右子树的边界
        root.right = recur(pre_root + (in_root - in_left) + 1, in_root+1, in_right);
        return root;
    }
}

 

实现二叉树的前序中序后序遍历 (BFS 和 DFS )

 

剑指 Offer 16. 数值的整数次方

  1. 当 x = 0 时:直接返回 0 (避免后续 x = 1 / x 操作报错)。
  2. 初始化 res = 1 ;
  3. 当 n < 0 时:把问题转化至 n≥0 的范围内,即执行 x = 1/x,n = - n ;
  4. 循环计算:当 n = 0时跳出;
      当 n & 1 = 1 时:将当前 x 乘入 res (即 res *= x );执行 x = x^2(即 x *= x );执行 n 右移一位(即 n>>=1)
  5. 返回 res 。
class Solution {
    public double myPow(double x, int n) {
        if(x == 0) return 0;
        long b = n;
        double res = 1.0;
        if(b < 0) {
            x = 1 / x;
            b = -b;
        }
        while(b > 0) {
            if((b & 1) == 1) res *= x;
            x *= x;
            b >>= 1;
        }
        return res;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/solution/mian-shi-ti-16-shu-zhi-de-zheng-shu-ci-fang-kuai-s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

视频讲解:https://www.bilibili.com/video/BV1FK4y1j73k

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

    private boolean recur(int[] postorder, int left, int right) {
        //(结束条件最后看!)左右指针重合的时候,即left~right区间只有一个数
        if (left >= right) {
            return true;
        }
        //在后序遍历中根节点一定是最后一个点
        int root = postorder[right];
        int index = left;
        while (postorder[index] < root) {
            index++;
        }
        int m = index;//left~right之间第一个比root大的点,即root右子树中最小的点(右子树后序遍历的起点)
        //如果m~right区间(root的右子树)出现了比root小的节点,则不可能是后序遍历
        while (index < right) {
            if (postorder[index] < root) {
                return false;
            }
            index++;
        }
        //此时能保证left ~ m-1都比root小,m ~ right-1都比root大,但这两个子区间内部的情况需要继续递归判断
        return recur(postorder, left, m - 1) && recur(postorder, m, right - 1);
    }

位运算(简单)

剑指 Offer 15. 二进制中1的个数

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0){
            res += n & 1; //n&1是1,最后一位是1
            n >>>= 1; //无符号右移为 ">>>"
        }
        return  res;
    }
}

剑指 Offer 65. 不用加减乘除做加法

参考:https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/solution/dian-zan-yao-wo-zhi-dao-ni-xiang-kan-dia-ovxy/

class Solution {
    public int add(int a, int b)
        //进位:与运算+左移一位
        //非进位和:异或运算
        // 2 + 3 = 5
        // 0011 a=3
        // 0010 b=2
        // 0010 a&b
        // 0100 a&b左移:代表进位
        // 0001 a=a^b 异或:代表非进位和
        // 进位 + 非进位和 = 两数之和
        while(b != 0){ //当进位为 0 时跳出
            //(a&b) << 1 :先按位与,再左移(相当于*2)
            int c = (a&b) << 1; /// c = 进位
            a ^= b; //a = a^b异或运算
            b = c;
        }
        return a;
    }
}

位运算(中等)

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

限制时间复杂度o(n),空间复杂度o(1),不能使用额外的空间。

使用异或。要明白两个十进制数字之间的异或,是对十进制数对应的二进制数的异或。相同取0,不同取1。

https://www.ruanyifeng.com/blog/2021/01/_xor.html

如果只有一个不同的数字,可以直接通过异或得到这个数字。本题是有两个不同的数字。

class Solution {
    public int[] singleNumbers(int[] nums) {
        //异或:相同的数字异或为0,任何数字与0异或结果是其本身。
        //所以遍历异或整个数组最后得到的结果就是两个只出现一次的数字异或的结果:即 z = x ^ y
        int z = 0;  
        for(int i : nums) z ^= i;
        //我们根据异或的性质可以知道:z中至少有一位是1,否则x与y就是相等的。
        //我们通过一个辅助变量m来保存z中哪一位为1.(可能有多个位都为1,我们找到最低位的1即可)。
        //举个例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.
        //我们将m初始化为1,如果(z & m)的结果等于0说明z的最低为是0
        //我们每次将m左移一位然后跟z做与操作,直到结果不为0.
        //此时m应该等于1000,同z一样,第四位为1.
        int m = 1;
        while((z & m) == 0) m <<= 1;
        //我们遍历数组,将每个数跟m进行与操作,结果为0的作为一组,结果不为0的作为一组
        //例如对于数组:[1,2,10,4,1,4,3,3],我们把每个数字跟1000做与操作,可以分为下面两组:
        //nums1存放结果为0的: [1, 2, 4, 1, 4, 3, 3]
        //nums2存放结果不为0的: [10] (碰巧nums2中只有一个10,如果原数组中的数字再大一些就不会这样了)
        //此时我们发现问题已经退化为数组中有一个数字只出现了一次
        //分别对nums1和nums2遍历异或就能得到我们预期的x和y
        int x = 0, y = 0;
        for(int i : nums) {
            //这里我们是通过if...else将nums分为了两组,一边遍历一遍异或。
            //跟我们创建俩数组nums1和nums2原理是一样的。
            if((i & m) == 0) x ^= i;
            else y ^= i;
        }
        return new int[]{x, y};
    }
}

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

对比上题:没有限制空间复杂度,可以使用哈希表。

简单做法:使用hashmap统计数字出现的次数,再遍历即可。

// 使用 HashMap 记录各个数字出现的次数
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap();

        for(int i = nums.length - 1; i >= 0; --i){
            int key = nums[i];
            if(!map.containsKey(key)){
                // 如果之前没有遇到这一数字,则放入 map 中
                map.put(key, 1);
            }else{
                // 如果之前遇到过这一数字,则出现次数加 1
                map.put(key, map.get(key) + 1);
            }
        }

        //entrySet是 java中 键-值 对的集合,Set里面的类型是Map.Entry
        for(Map.Entry<Integer, Integer> entry: map.entrySet()){
            if(entry.getValue() == 1){
                return entry.getKey();
            }
        }

        return -1;
    }

有限状态自动机,没看懂。

class Solution {
    public int singleNumber(int[] nums) {
        int ones = 0, twos = 0;
        for(int num : nums){
            ones = ones ^ num & ~twos;
            twos = twos ^ num & ~ones;
        }
        return ones;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/solution/mian-shi-ti-56-ii-shu-zu-zhong-shu-zi-chu-xian-d-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

day23 数学(简单)

剑指 Offer 39. 数组中出现次数超过一半的数字

摩尔投票法:

class Solution {
    public int majorityElement(int[] nums) {
        //始时将众数标记为 nums[0] 且 count 设置为 1
        int cand_num = nums[0], count = 1;
        for (int i = 1; i < nums.length; ++i) {
            if (cand_num == nums[i])
                ++count;
            else if (--count == 0) {
                cand_num = nums[i];
                count = 1;
            }
        }
        return cand_num;
    }
}

作者:gfu
链接:https://leetcode.cn/problems/majority-element/solution/3chong-fang-fa-by-gfu-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 排序:

class Solution {
    public int majorityElement(int[] nums) {
        Arrays.sort(nums);
        return nums[nums.length >> 1];
    }
}

 

剑指 Offer 66. 构建乘积数组

class Solution {
    public int[] constructArr(int[] a) {
        /*
        参考"数据结构与算法"的题解
        B[i]实际上是i左边元素的乘积*i右边元素的乘积
        例如:[1,2,3,4,5,6],B[2]=(1*2)*(4*5*6)
        那么我们设A[i]左边的乘积为resLeft[i],右边的乘积为resRight[i](均不包含A[i])
        显然res[i]=resLeft[i]*resRight[i],而resLeft[i]与resRight[i]可以由迭代乘积求得
        */
      
        if(a == null || a.length == 0) {
            return new int[0];
        }
        int len = a.length;
        // 结果
        int[] res = new int[len];
        // A[i]左边的乘积(不包含A[i])
        int[] resLeft = new int[len];
        // 初始化A[0]左边的乘积为1
        resLeft[0] = 1;
        // 遍历范围为:[1, len - 1]
        for(int i = 1; i < len; i++) {
            // A[i]左边的乘积=A[i-1]左边(不含A[i-1])的乘积*A[i-1]
            // eg:[1,2,3,4,5,6],resLeft[2]=1*2,resLeft[3]=(1*2)*3=resLeft[2]*A[2]
            // 即resLeft[i] = resLeft[i - 1] * a[i - 1]
            resLeft[i] = resLeft[i - 1] * a[i - 1];
        }
        // A[i]右边的乘积(不包含A[i])
        int[] resRight = new int[len];
        // 初始化A[len - 1]右边的乘积为1
        resRight[len - 1] = 1;
        for(int i = len - 2; i >= 0; i--) {
            // A[i]右边的乘积=A[i+1]右边(不含A[i+1])的乘积*A[i+1]
            // eg:[1,2,3,4,5,6],resRight[3]=5*6,resRight[2]=(5*6)*4=resRight[3]*A[3]
            // 即resRight[i] = resRight[i + 1] * a[i + 1]
            resRight[i] = resRight[i + 1] * a[i + 1];
        }
        // 最终将A[i]左边的乘积*A[i]右边边的乘积就是不含A[i]的其他乘积
        for(int i = 0; i < len; i++) {
            res[i] = resLeft[i] * resRight[i];
        }
        return res;
    }
}

 

第 24 天 数学(中等)

剑指 Offer 14- I. 剪绳子

class Solution {
    public int integerBreak(int n) {
        //选用尽量多的3,直到剩下2或者4时,用2
        if(n <= 3) return n - 1;        
        int a = n / 3 , b = n % 3;
        if(b == 0) return (int)Math.pow(3,a);
        if(b == 1) return (int)Math.pow(3,a-1) * 4;
        return (int)Math.pow(3,a)*2; ///3,3,2->3^2 *2
    }
}

 

剑指 Offer 57 - II. 和为s的连续正数序列

public int[][] findContinuousSequence(int target) {
    int i = 1; // 滑动窗口的左边界
    int j = 1; // 滑动窗口的右边界
    int sum = 0; // 滑动窗口中数字的和
    List<int[]> res = new ArrayList<>();

    while (i <= target / 2) {
        if (sum < target) {
            // 右边界向右移动
            sum += j;
            j++;
        } else if (sum > target) {
            // 左边界向右移动
            sum -= i;
            i++;
        } else {
            // 记录结果
            int[] arr = new int[j-i];
            for (int k = i; k < j; k++) {
                arr[k-i] = k;
            }
            res.add(arr);
            // 左边界向右移动
            sum -= i;
            i++;
        }
    }

    return res.toArray(new int[res.size()][]);
}

作者:nettee
链接:https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/solution/shi-yao-shi-hua-dong-chuang-kou-yi-ji-ru-he-yong-h/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

剑指 Offer62. 圆圈中最后剩下的数字

https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/huan-ge-jiao-du-ju-li-jie-jue-yue-se-fu-huan-by-as/

约瑟夫问题。

class Solution {
    public int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans;
    }
}

作者:sweetieeyi
链接:https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

递归做法:

class Solution {
    public int lastRemaining(int n, int m) {
        if(n == 1) return 0;
        return (lastRemaining(n - 1, m) + m) % n;
    }
}

第 25 天模拟(中等)

剑指 Offer 29. 顺时针打印矩阵

class Solution {
    int[][] direct = {{0,1},{1,0},{0,-1},{-1, 0}};
    public int[] printMatrix(int[][] matrix) {
        if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return new int[0];
        }
        int m = matrix.length, n = matrix[0].length;
        boolean[][] access = new boolean[m][n];
        int x = 0, y = 0, d = 0;
        int[] result = new int[m * n];
        for(int i = 0; i < result.length; i++){
            
            result[i] = matrix[x][y];/////bug
            
            access[x][y] = true;
            int a = x + direct[d][0], b = y + direct[d][1];
            if(a < 0 || a >= m || b < 0 || b >= n || access[a][b]){
                d = (d + 1) % 4;
                a = x + direct[d][0];
                b = y + direct[d][1];
            }
            x = a;
            y = b;
        }
        return result;
    }
}

作者:菜鸡小叶
链接:https://www.acwing.com/activity/content/code/content/333529/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

力扣题解:

//小白注释版
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        //判空
        if(matrix.length == 0)
            return new int[0];

        //初始化边界值 和 结果数组的当前指针
        int left=0, right=matrix[0].length-1, top=0, bottom=matrix.length-1;
        int cur = 0;

        //初始化结果数组
        int[] res = new int[(right+1) * (bottom+1)];

        //死循环:走一条边;更新边界值;判断边界值交错则跳出;
        while(true){//注意:行top或bottom,列left或right
            //从左到右: top行, left++列
            for(int i = left; i<=right; i++){
                res[cur] = matrix[top][i];
                cur++;
            }

            //更新边界值(top+1), 判断更新后的边界值是否交叉: 等于不算交叉
            top++;
            if(top>bottom)
                break;

            //从上到下:top++行,right列
            for(int i = top; i<=bottom; i++){
                res[cur] = matrix[i][right];
                cur++;
            }

            //更新边界值(right-1),并判断更新后的边界值是否交叉
            right--;
            if(right<left)
                break;

            //从右到左: bottom行,right--列
            for(int i = right; i>=left; i--){
                res[cur] = matrix[bottom][i];
                cur++;
            }

            //更新边界值(bottom-1),并判断更新后的边界值是否交叉
            bottom--;
            if(bottom<top)
                break;

            //从下到上: bottom--行, left列
            for(int i = bottom; i >= top ; i--){
                res[cur] = matrix[i][left];
                cur++;
            }

            //更新边界值(left+1),并判断更新后的边界值是否交叉
            left++;
            if(left>right)
                break;
        }

        //返回结果数组
        return res;
    }
}

 

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

用栈模拟栈混洗。
遍历入栈元素序列,先将最先要入栈的元素入栈,然后判断当前栈顶元素是否等index下标指向的出栈序列元素,相等则出栈,index++,继续判断。不等则继续入栈,重复操作。
循环结束,栈空则说明栈混洗序列合法,否则不合法。

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        if(pushed == null && popped == null) return true;
        if(pushed.length != popped.length) return false;
        Stack<Integer> stack = new Stack();
        int index = 0;
        for (int num : pushed) {//遍历 入栈序列
            stack.push(num);
            while(!stack.isEmpty() && stack.peek() == popped[index]) {
                stack.pop();
                index++;
            }
        }
        return stack.isEmpty();
    }
}

第 26 天 字符串(中等)

剑指 Offer 20. 表示数值的字符串

查看代码
 class Solution {
    public boolean isNumber(String s) {
        if(s == null || s.length() == 0) return false; // s为空对象或 s长度为0(空字符串)时, 不能表示数值
        boolean isNum = false, isDot = false, ise_or_E = false; // 标记是否遇到数位、小数点、‘e’或'E'
        char[] str = s.trim().toCharArray();  // 删除字符串头尾的空格,转为字符数组,方便遍历判断每个字符
        for(int i=0; i<str.length; i++) {
            if(str[i] >= '0' && str[i] <= '9') isNum = true; // 判断当前字符是否为 0~9 的数位
            else if(str[i] == '.') { // 遇到小数点
                if(isDot || ise_or_E) return false; // 小数点之前可以没有整数,但是不能重复出现小数点、或出现‘e’、'E'
                isDot = true; // 标记已经遇到小数点
            }
            else if(str[i] == 'e' || str[i] == 'E') { // 遇到‘e’或'E'
                if(!isNum || ise_or_E) return false; // ‘e’或'E'前面必须有整数,且前面不能重复出现‘e’或'E'
                ise_or_E = true; // 标记已经遇到‘e’或'E'
                isNum = false; // 重置isNum,因为‘e’或'E'之后也必须接上整数,防止出现 123e或者123e+的非法情况
            }
            else if(str[i] == '-' ||str[i] == '+') { 
                if(i!=0 && str[i-1] != 'e' && str[i-1] != 'E') return false; // 正负号只可能出现在第一个位置,或者出现在‘e’或'E'的后面一个位置
            }
            else return false; // 其它情况均为不合法字符
        }
        return isNum;
    }
}

剑指 Offer 67. 把字符串转换成整数

查看代码
 class Solution {
    public int strToInt(String str) {
        char[] s = str.toCharArray();
        int n = s.length;

        int k = 0;
        while (k < n && s[k] == ' ') k++;
        if (k == n) return 0;

        boolean isMinus = false;
        if (s[k] == '+') k++;
        else if (s[k] == '-') {
            isMinus = true;
            k++;
        }

        long res = 0;
        while (k < n && Character.isDigit(s[k])) {
            res = res * 10 + (s[k++] - '0');
            if (!isMinus && res > Integer.MAX_VALUE) return Integer.MAX_VALUE;
            if (isMinus && -res < Integer.MIN_VALUE) return Integer.MIN_VALUE;
        }

        if (isMinus) return (int) -res;
        return (int) res;
    }
}

作者:sp
链接:https://www.acwing.com/activity/content/code/content/1134254/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
    public int strToInt(String str) {
        int res = 0, bndry = Integer.MAX_VALUE / 10;
        int i = 0, sign = 1, length = str.length();
        if(length == 0) return 0;
        while(str.charAt(i) == ' ')
            if(++i == length) return 0;
        if(str.charAt(i) == '-') sign = -1;
        if(str.charAt(i) == '-' || str.charAt(i) == '+') i++;
        for(int j = i; j < length; j++) {
            if(str.charAt(j) < '0' || str.charAt(j) > '9') break;
            if(res > bndry || res == bndry && str.charAt(j) > '7')
                return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            res = res * 10 + (str.charAt(j) - '0');
        }
        return sign * res;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/solution/mian-shi-ti-67-ba-zi-fu-chuan-zhuan-huan-cheng-z-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

栈与队列(困难)

剑指 Offer 59 - I. 滑动窗口的最大值

查看代码
 class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        if (len == 0) return new int[0];
        int[] ans = new int[len - k + 1];
        int[] queue = new int[len];
        int pre = 0, top = 0;
        //单调递减的队列
        for (int i = 0; i < len; ++i) {
            if(i - queue[pre] >= k) pre++;
            //比当前小的全都出队列
            while(top > pre && nums[queue[top - 1]] < nums[i])
                top--;
            queue[top ++] = i;
            if(i >= k - 1)
                ans[i - k + 1] = nums[queue[pre]];
        }
        return ans;
    }
}

 

面试题59 - II. 队列的最大值

class MaxQueue {
    Queue<Integer> queue;
    Deque<Integer> deque;
    public MaxQueue() {
        queue = new LinkedList<>();
        deque = new LinkedList<>();
    }
    public int max_value() {
        return deque.isEmpty() ? -1 : deque.peekFirst();
    }
    public void push_back(int value) {
        queue.offer(value);
        while(!deque.isEmpty() && deque.peekLast() < value)
            deque.pollLast();
        deque.offerLast(value);
    }
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        if(queue.peek().equals(deque.peekFirst()))
            deque.pollFirst();
        return queue.poll();
    }
}

作者:jyd
链接:https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/solution/jian-zhi-offer-59-ii-dui-lie-de-zui-da-z-0pap/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

搜索与回溯算法(困难)

剑指 Offer 37. 序列化二叉树

超时

public class Codec {

    int index = -1;
    // Encodes a tree to a single string.
    String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        if(root == null) {
            sb.append("null,");
            return sb.toString();
        }
        sb.append(root.val).append(",");
        sb.append(serialize(root.left));
        sb.append(serialize(root.right));
        return sb.toString();
    }

    // Decodes your encoded data to tree.
    TreeNode deserialize(String data) {
        index++;
        String[] nodes = data.split(",");
        TreeNode node = null;
        if(!nodes[index].equals("null")) {
            node = new TreeNode(Integer.valueOf(nodes[index]));
            node.left = deserialize(data);
            node.right = deserialize(data);
        }
        return node;
    }
}

 

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {
    public String serialize(TreeNode root) {
        if(root == null) return "[]";
        StringBuilder res = new StringBuilder("[");
        Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
        while(!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if(node != null) {
                res.append(node.val + ",");
                queue.add(node.left);
                queue.add(node.right);
            }
            else res.append("null,");
        }
        res.deleteCharAt(res.length() - 1);
        res.append("]");
        return res.toString();
    }

    public TreeNode deserialize(String data) {
        if(data.equals("[]")) return null;
        String[] vals = data.substring(1, data.length() - 1).split(",");
        TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
        Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
        int i = 1;
        while(!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if(!vals[i].equals("null")) {
                node.left = new TreeNode(Integer.parseInt(vals[i]));
                queue.add(node.left);
            }
            i++;
            if(!vals[i].equals("null")) {
                node.right = new TreeNode(Integer.parseInt(vals[i]));
                queue.add(node.right);
            }
            i++;
        }
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

 

剑指 Offer 38. 字符串的排列

class Solution {
    public String[] permutation(String s) {
        Set<String> list = new HashSet<>();
        char[] arr = s.toCharArray();
        StringBuilder sb = new StringBuilder();
        boolean[] visited = new boolean[arr.length];
        dfs(arr, "", visited, list);
        return list.toArray(new String[0]);
    }
    public void dfs(char[] arr, String s,  boolean[] visited, Set<String> list)
    {
        if(s.length() == arr.length)
        {
            list.add(s);
            return;
        }
        for(int i=0; i<arr.length; i++)
        {
            if(visited[i]) continue;
            visited[i] = true;
            dfs(arr, s+String.valueOf(arr[i]), visited, list);
            visited[i] = false;
        }

    }
}

 

class Solution {
    
    /**
        该题类似于 全排列2,本题使用set来去除重复元素
        除了使用set去重外,还可以对数组进行排序,使用visited数组进行剪枝!

    */
    Set<String> res = new HashSet();
    public String[] permutation(String s) {

        backtrack(s.toCharArray(),new StringBuilder(), new boolean[s.length()]);
        return res.toArray(new String[0]); 

    }
    
    // 回溯函数
    public void backtrack(char[] ch,StringBuilder sb, boolean[] visitid){
        // 终止条件
        if(sb.length() == ch.length){
            res.add(sb.toString());
            return;
        }
        // 选择列表
        for(int i = 0; i < ch.length; i++){
            // 剪枝,如果当前位置的元素已经使用过,则跳过进入下一个位置
            if(visitid[i]) continue;
            // 做出选择
            sb.append(ch[i]);
            // 更新标记
            visitid[i] = true;
            // 进入下层回溯
            backtrack(ch,sb,visitid);
            // 撤销选择
            sb.deleteCharAt(sb.length()-1);
            visitid[i] = false;
            
        }
    }
}

相似题目:47. 全排列 II

acwing

class Solution {
    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums); ///将数字排序后,通过相同数字的稳定性,可以保证排列不重复
        dfs(nums, 0, new ArrayList<>(), new int[nums.length]);
        return list;
    }
    void dfs(int[] nums, int count, List<Integer> tList, int[] used)
    {
        int n = nums.length;
        if(count == n)
        {
            list.add(new ArrayList<>(tList));
            return;
        }
        for(int i = 0; i < n; i++)
        {
            if(used[i] > 0) continue;
            //当前数字和前面数字相同,前一个节点已经回溯过,就剪枝
            if(i > 0 && nums[i - 1] == nums[i] && used[i - 1] == 0) continue;
            used[i] = 1;
            tList.add(nums[i]);
            dfs(nums, count + 1, tList, used);
            tList.remove(tList.size() - 1);//回溯
            used[i] = 0;
        }
    }
}

leetcode题解

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used = new boolean[nums.length];
    // 排序,去重的基础
    Arrays.sort(nums);
    dfs(nums, path, res, used);
    return res;
}

private void dfs(int[] nums,
                    List<Integer> path,
                    List<List<Integer>> res,
                    boolean[] used) { // O(n)
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        // 剪枝,判断重复使用的数字
        if (used[i]) continue;
        // 去重的条件
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
        path.add(nums[i]);
        used[i] = true;
        dfs(nums, path, res, used);
        // 回溯的过程中,将当前的节点从 path 中删除
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

作者:tangweiqun
链接:https://leetcode.cn/problems/permutations-ii/solution/jian-dan-yi-dong-java-cpythonjsgo-quan-p-mt9u/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

动态规划(困难)

剑指 Offer 19. 正则表达式匹配

class Solution {
    public boolean isMatch(String s, String p) {
        /*
        dp五部曲:(参考最高赞的思路)
        设原始串s的长度为m,模式串p的长度为n
        注意定义:'*'表示它前面的(一个)字符可以出现任意次(含0次)!注意是一个
        1.状态定义:设dp[i][j]为考虑s[0,i-1],p[0,j-1]时,是否能匹配上,匹配上就为true
        2.状态转移:
            2.1 p[j-1]为非'*'
                2.1.1 若p[j-1]==s[i-1](必定为'a'-'z'),继续看dp[i-1][j-1]
                2.1.2 若p[j-1]为'.',直接看dp[i-1][j-1]
            2.2 p[j-1]为'*'
                2.2.1 若p[j-2]==s[i-1](必定为'a'-'z'),则继续向前看dp[i-1][j]
                    因为对于p[0,j-1]来说,s[i-1]是冗余匹配可以由p[j-2]*补充
                2.2.2 p[j-2]为'.',则'.'匹配了s[i-1],可以继续往前看dp[i-1][j]
                    注意这里是".*"的情形,也就是"万能串",生成"......"可以匹配任何非空s
                2.2.3 此时p[j-2]为'a'-'z',且p[j-2]!=s[i-1],"p[j-2]*"直接废掉,看dp[i][j-2]
            其中2.1.1和2.1.2可以合并为一种情形;2.2.1和2.2.2可以合并为一种情形
        3.初始化:
            3.1 空的p 
                3.1.1 可以匹配空的s,dp[0][0]=true
                3.1.2 不可以匹配非空的s,dp[i][0]=false,i∈[1,m-1]
            3.2 空的s
                3.2.1 可以匹配空的s,dp[0][0]=true
                3.2.2 可能可以匹配非空的p,要经过计算如"a*b*c*"
            3.3 非空的p与非空的s,要经过计算
        4.遍历顺序:显然是正序遍历
        5.返回形式:直接返回dp[m][n]就表示考虑s[0,m-1],j[0,n-1]是否能匹配上
        */
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        for(int i = 0; i <= m; i++) {
            for(int j = 0; j <= n; j++) {
                // 空的p的情形
                if(j == 0) {
                    // dp[0][0]=true;其余为false
                    dp[i][j] = i == 0;
                }else {
                    // 非空的p都是要经过计算的         
                    if(p.charAt(j - 1) != '*') {
                        // 1.p[j-1]!='*'的情形(2.1)
                        // 此时j>=1
                        if(i >= 1 && (s.charAt(i - 1) == p.charAt(j - 1) || 
                        p.charAt(j - 1) == '.')) {
                            // p与s都向前一位匹配
                            dp[i][j] = dp[i - 1][j - 1];
                        }
                    }else {
                        // 2.p[j-1]=='*'的情形(2.2)
                        // 看"x*"的情形(2.2.1与2.2.2)
                        if(i >= 1 && j >= 2 && 
                        (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.')) {
                            // p不动s向前一位
                            dp[i][j] = dp[i - 1][j];
                        }
                        // 不看"x*"的情形(2.2.3)
                        // 此时p[j-2]为'a'-'z',且p[j-2]!=s[i-1],"p[j-2]*"直接废掉
                        if(j >= 2) {
                            // s不变p向前2位
                            dp[i][j] |= dp[i][j - 2];
                            // 这里用|=的原因是:2.2中满足任意一种情形就可以判定dp[i][j]=true
                        }
                    }
                }
            }
        }
        return dp[m][n];
    }
}

剑指 Offer 49. 丑数

https://leetcode.cn/problems/chou-shu-lcof/solution/mian-shi-ti-49-chou-shu-dong-tai-gui-hua-qing-xi-t/616186

class Solution {
    public int nthUglyNumber(int n) {
        int[] dp = new int[n];  // 使用dp数组来存储丑数序列
        dp[0] = 1;  // dp[0]已知为1
        int a = 0, b = 0, c = 0;    // 下个应该通过乘2来获得新丑数的数据是第a个, 同理b, c

        for(int i = 1; i < n; i++){
            // 第a丑数个数需要通过乘2来得到下个丑数,第b丑数个数需要通过乘2来得到下个丑数,同理第c个数
            int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n2, n3), n5);
            if(dp[i] == n2){
                a++; // 第a个数已经通过乘2得到了一个新的丑数,那下个需要通过乘2得到一个新的丑数的数应该是第(a+1)个数
            }
            if(dp[i] == n3){
                b++; // 第 b个数已经通过乘3得到了一个新的丑数,那下个需要通过乘3得到一个新的丑数的数应该是第(b+1)个数
            }
            if(dp[i] == n5){
                c++; // 第 c个数已经通过乘5得到了一个新的丑数,那下个需要通过乘5得到一个新的丑数的数应该是第(c+1)个数
            }
        }
        return dp[n-1];
    }
}

剑指 Offer 60. n个骰子的点数

类似题目:https://www.acwing.com/problem/content/76/ 视频讲解:https://www.acwing.com/video/203/

题解:【n个骰子的点数】:详解动态规划及其优化方式 - n个骰子的点数 - 力扣(LeetCode)

代码:

class Solution {
    public double[] dicesProbability(int n) {
        int[][] dp = new int[n + 1][n * 6 + 1];
        for(int i = 1; i <= 6; i++){
            dp[1][i] = 1;
        }
        for(int i = 2; i <= n; i++){
            for(int j = 1; j <= 6 * i; j++){
                for(int k = 1; k <= 6 && j - k > 0; k++){
                    dp[i][j] += dp[i - 1][j - k];
                }
            }
        }
        double[] result = new double[6 * n - n + 1];
        for(int i = 0; i < result.length; i++){
            result[i] = dp[n][i + n] / 6.0;
        }
        return result;
    }
}

分治算法(困难)

剑指 Offer 17. 打印从1到最大的n位数

主要考大数问题,用字符串模拟加法。

class Solution {
    int[] res;
    int nine = 0, count = 0, start, n;
    char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    public int[] printNumbers(int n) {
        this.n = n;
        res = new int[(int)Math.pow(10, n) - 1];
        num = new char[n];
        start = n - 1;
        dfs(0);
        return res;
    }
    void dfs(int x) {
        if(x == n) {
            String s = String.valueOf(num).substring(start);
            if(!s.equals("0")) res[count++] = Integer.parseInt(s);
            if(n - start == nine) start--;
            return;
        }
        for(char i : loop) {
            if(i == '9') nine++;
            num[x] = i;
            dfs(x + 1);
        }
        nine--;
    }
}

作者:jyd
链接:https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/solution/mian-shi-ti-17-da-yin-cong-1-dao-zui-da-de-n-wei-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 51. 数组中的逆序对

视频:https://www.acwing.com/video/189/

class Solution {
    public int reversePairs(int[] nums) {
        return mergesort(nums, 0, nums.length-1);
    }

    int mergesort(int[] nums,int l,int r){
        if(l >= r) return 0;
        int mid = l + r >> 1;
        int res=mergesort(nums,l,mid)+mergesort(nums,mid+1,r); 
        int k=0;
        int i=l, j = mid+1;
        ArrayList<Integer> t =new ArrayList<>();
        while(i <=mid && j<=r){// i < mid + 1 || j < r + 1
            if(nums[i] <= nums[j]) t.add(nums[i++]);
            else
            {
                //现在数组[5,6,7,8],[1,2,3,4]
                t.add(nums[j++]);
                res += mid - i + 1;
            }
        }
        while(i<=mid)t.add(nums[i++]);
        while(j<=r)t.add(nums[j++]);

        k=l;
        for(int x:t){
            nums[k++]=x;
        }

        return res;
    }
}

数学(困难)

剑指 Offer 14- II. 剪绳子 II

循环求余公式解法。能够轻松与上一题代码风格保持高度一致。循环求余比快速幂求余理解难度更低。

// 求 (x^a) % p —— 循环求余法。固定搭配建议背诵
   public long  remainder(int x,int a,int p){  //x为底数,a为幂,p为要取的模
        long rem = 1 ;
        for (int i = 0; i < a; i++) {
            rem = (rem * x) % p ;   
        }
        return rem;
    }
class Solution {
   public int cuttingRope(int n) {
        if(n <= 3) return n - 1;
        int b = n % 3, p = 1000000007;

        long rem = 1, x = 3 ,a = n / 3;
        //直接套循环求余公式
        for(int i = 0; i < ((b == 1)?a-1:a); i++) { //b == 1代表余数为1的时候,需要单独取出一个3出来凑成2*2达到最大值效果
            rem = (rem * x) % p;
        }  
        if(b == 0) return (int)(rem % p);
        if(b == 1) return (int)(rem * 4 % p);
        return (int)(rem * 2 % p);
    }
}

剑指 Offer 43. 1~n 整数中 1 出现的次数

参考讲解:https://www.acwing.com/video/180/

代码:

class Solution {
    public int numberOf1Between1AndN_Solution(int n) {
        List<Integer> nums = new ArrayList<>();
        while(n > 0){
            nums.add(n % 10);
            n /= 10;
        }
        Collections.reverse(nums);// 从高位到低位存放
        int count = 0;
        for(int i = 0; i < nums.size(); i++){
            int pre = 0, next = 0; //其之前组成的值 与其之后组成的值例如  215 4 214 若 i = 3 ,pre = 215 next = 214
            for(int j = 0; j < i; j++){
                pre = pre * 10 + nums.get(j);
            }
            for(int j = i + 1; j < nums.size(); j++){
                next = next * 10 + nums.get(j);
            }
            //得出pre和next的值
            // System.out.println(pre + " " + next);
            //nums.size() - i - 1,以215 4 214为例,7-3-1=3.
            count += pre * Math.pow(10, nums.size() - i - 1);
            if(nums.get(i) == 1){//如果第i位是1,count加上后面部分的所有数字
                count += next + 1;
            }else if(nums.get(i) > 1){//并行条件,如果大于1时候,可以取第i位为1时候的情况。
                count += Math.pow(10, nums.size() - i - 1);
            }
        }
        return count;
    }
}

作者:菜鸡小叶
链接:https://www.acwing.com/activity/content/code/content/340995/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 44. 数字序列中某一位的数字

视频:https://www.acwing.com/video/181/

class Solution {
    public int findNthDigit(int n) {
        if(n==0) return 0;
        //由于是n=0时对应开始的0,这里不需要进行减操作n--;,但是如果n=1对应开始的0则需要减操作
        //排除n=0后,后面n从1开始。
        int digit = 1;
        int start = 1;
        long count = 9; //count的值有可能会超出int的范围,所以变量类型取为long
        while(n>count){//不能带=号,此时n从1开始,n=count时,属于上一个循环的最后一个数字
            n=(int)(n-count);//这里(int)不能省略
            digit++;
            start = start*10;
            count = (long)start*9*digit;
            //这里的long不能省略,否则,会先按照int类型进行计算,然后赋值给long型的count,超过int大小限制时,会出现负数
        }

        int num = start + (n-1)/digit;
        int index = (n-1)%digit;//index最大取digit-1,即此时num坐标从左到右为0,1,...,digit-1,共digit位
        while(index<(digit-1)){
        //最后的结果是num中的第index个数字,index从左到右从0开始递增,考虑到踢出右侧末尾的数字比较简单,我们从右侧开始依次踢出数字
            num = num/10;
            digit--;
        }
        return num%10;//此时num的右侧末尾数字即为结果
    }
}

其他

不修改数组找出重复的数字

分治+抽屉原理
class Solution {
    public int duplicateInArray(int[] nums) {
        int l = 1;
        int r = nums.length - 1;//nums数组大小是n+1,r其实是题目中所说的n
        while(l < r) {
            int mid = l + r >> 1;
            int count = 0;
            for(int num : nums) {
                if(num >= l && num <= mid) count++;
            }
            if(count > mid + 1 -l) r = mid;
            else l = mid + 1;
        }
        return l;
    }
}

不修改数组找出重复的数字

二叉树的下一个结点

根据当前节点找到中序遍历的下一个节点,大概思路

中序遍历:左根右。可以分为节点p在当前子树的左,根,右,分情况讨论。

当前节点在 某个子树 的左节点,可知中序遍历下一个节点是 当前字数的根节点。

当前节点在 某个子树 的根节点,右子树中最左子树的节点,是中序的下个节点。

当前节点在 某个子树 的右节点,并且父节点不为空,递归找父节点,直到当前结点在其父节点的左子树位置处。如果没有合适的父节点返回null。

class Solution {
    public TreeNode inorderSuccessor(TreeNode p) {
        //p节点有右子节点
        if(p.right != null){
            TreeNode temp = p.right;
            while(temp.left != null){
                temp = temp.left;
            }
            return temp;
        }
        if(p.father == null) return null;
        //p节点没有右子节点,但p节点是父节点的左孩子
        if(p.father.left == p) return p.father;
        //p节点没有右子节点,且p节点是父节点的右孩子
        //从p节点的父节点开始,直到该节点是其父节点的左孩子
        while(p.father != null && p.father.right == p){
            p = p.father;
        }
        return p.father;
    }
}

posted on 2022-05-15 17:14  passionConstant  阅读(158)  评论(0编辑  收藏  举报