代码随想录(每日更新)

数组

二分查找

35. 搜索插入位置

关键词:排序数组、无重复、升序、logn。

思路:根据关键词,我们可以很容易地想到二分的算法,对于找值的索引,还是比较常规,特点是,如果找不到该数据,需要指定需要插入的位置。有这么几种情况。

  1. 比所有值都小,直接插入0;
  2. 比所有值都大,直接插入len(right + 1)
  3. 找到该值,直接插入mid;
  4. 找不到该值,所以为right + 1或者left,如果最后一个双指针重合的值,比查找的值大,left++(因为left = rigth),最后插入位置为left = right + 1;如果最后一个双指针重合的值,比查找的值小,right--,那么最后插入的位置为left = right + 1)
class Solution {
    public int searchInsert(int[] nums, int target) {
        //1. 获取数组长度
        int len = nums.length;

        //2. 提前退出
        if (target < nums[0]) {
            return 0;
        }

        if (target > nums[len - 1]) {
            return len;
        }

        int left = 0, right = len - 1;
        int mid = 0;

        //3. 使用==
        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        //4. left 或者 right + 1;
        return left;
    }
}

34. 在排序数组中查找元素的第一个和最后一个位置

思路:依旧是二分查找,只不过,这不是直接找到值就返回,这个而是找到值后,往左右去二分逼近,分别写两个函数,一个是找左边界,一个是找右边界。例如找左边界,找到值后,需要继续往左边找,即right = mid - 1;

要合并的话,只需要看两个方法,有什么区别,简单判断一下即可。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = 0; int right = nums.length - 1;
        int lres = -1; int rres = -1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                right = mid - 1;
                lres = mid;
            } else if (nums[mid] < target){
                left = mid + 1;
            }  else {
                right = mid - 1;
            }
        }

        left = 0; right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                left = mid + 1;
                rres = mid;
            } else if (nums[mid] > target){
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return new int[]{lres, rres};
    }
}

image-20230603213246716

69. x 的平方根

思路:利用二分法,去逼近那个值,当最接近的小于那个x值的时候,就是要求的值,如果mid mid大于x,那么一定不是,但是mid mid小于x,那么就是在逼近。

class Solution {
    public int mySqrt(int x) {
        int left = 0; int right = x;
        int res = 0;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            long sum = (long)mid * mid;
            if (sum == x) {
                return mid;
            } else if (sum < x) {
                left = mid + 1;
                //逼近的过程
                res = mid;
            } else {
                right = mid - 1;
            }
        }
        return res;
    }
}

image-20230603213607957

367. 有效的完全平方数

思路:有了上面一题做铺垫,这题就很简单啦,上面一题是,找到最近的,所以精度还不是准确的,而这题是精度准确的,只需要==的时候直接返回true,其他都是false即可。

class Solution {
    public boolean isPerfectSquare(int num) {
        int left = 0; int right = num;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            long sum = (long) mid * mid;
            if (sum == num) {
                return true;
            } else if (sum < num) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return false;
    }
}

image-20230603213911048

移除元素

27. 移除元素

关键词:原地,所以你也就没能有声明另外一个空间的机会。

思路:我们需要怎么维护数组的实际长度呢,是不是可以利用一个指针,指向当前正确数组的位置(即没有对应元素的数组),还有一个末尾的指针,就是为了把后面的元素,搬到前面来,然后末尾的指针位移的长度,就是对应要删除元素的个数。

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0; int right = nums.length - 1;
        while (left <= right) {
            if (nums[left] == val) {
                nums[left] = nums[right--];
            } else {
                ++left;
            }
        }
        return left;
    }
}

image-20230603214312060

26. 删除有序数组中的重复项

我自己看到题目后的想法:

思考:这个题目,和上面的题目有点类似,但是又感觉完全不一样,因为上面一题是无序的,这一题是要保证最后的结果的相对位置不受影响,所以不能保持一个尾指针进行交换了。这题的思路应该是,一个指针保证前面的都是有序且不重复的,然后另一个指针去遍历,找到第一个不重复的结点即可(last与pre的值进行比较),然后对其进行覆盖(不能交换,看一下比较条件,交换后,等会依旧成立,等会就会一直交换)。

class Solution {
    public int removeDuplicates(int[] nums) {
        int len = nums.length;

        //不会重复,直接退出
        if (len == 1) {
            return 1;
        }

        int pre = 0;
        int last = 1;
        //保证不越界
        while (last < len) {
            //不相等的话,表示last是找到了不重复的地方了,那直接覆盖掉pre下一个.这个也可以包含第一次循环的情况.那么也就是自己覆盖自己
            if (nums[pre] != nums[last]) {
                nums[pre + 1] = nums[last];
                last++;
                pre++;
            } else {
                //继续寻找
                last++;
            }
        }
        return pre + 1;
    }
}

// 最终优化代码
class Solution {
    public int removeDuplicates(int[] nums) {
        int left = 0; int right = 0;
        while (right < nums.length) {
            if(nums[left] != nums[right]) nums[left++ + 1] = nums[right];
            ++right;
        }
        return left + 1;
    }
}

image-20230603215506925

283. 移动零

思路:又是一道双指针的题目,每次遇到双指针的题目,都是一个是指明位置,一个是遍历的指针指明位置的指针是需要的结果的指针,遍历的指针是条件的指针。这题,我们可以知道,结果是前面都是非0 的值,所以我们用一个指针去指定位置(剔除0的尾部),另一个是去找到非0的第一个,也尾部进行交换,因为尾部就是0。

//第一版代码
class Solution {
    public void moveZeroes(int[] nums) {
        int len =  nums.length;

        int pre = 0;
        int last = 0;

        while (last < len) {

            //pre不为0,都往前走
            if (nums[pre] != 0) {
                ++pre;
                ++last;
            } 
            //pre==0,并且last==0
            else if(nums[last] == 0) {
                ++last;
            } 
            //last找到非0
            else {
                nums[pre] -= nums[last];
                nums[last] += nums[pre];
                nums[pre] = nums[last] - nums[pre];
                ++pre;
                ++last;
            }
        }
    }
}

//提取一下公共部分
class Solution {
    public void moveZeroes(int[] nums) {
        int len =  nums.length;

        int pre = 0;
        int last = 0;

        while (last < len) {
            //pre先不用动,等到被交换了,不为0,才需要动.
            if (nums[last] != 0) {
                int temp = nums[pre];
                nums[pre] = nums[last];
                nums[last] = temp;
                ++pre;
            }
            ++last;
        }
    }
}

image-20230603220037773

844. 比较含退格的字符串

思路一:比较朴素,直接去重构字符串,正常字符就添加,是#就退一,等会比较最后的字符串即可,对于重构字符串,我们可以利用stringbuider,充当一个栈来使用。

思路二:可以分析到,#只会影响前面的字符,后面的不会影响,所以直接可以从后面开始比较,然后有#号,就可以跳过,而且可以同时比较

//这个代码很丑, 但是是我的第一思路
class Solution {
    public boolean backspaceCompare(String s, String t) {

        char[] s1 = new char[s.length()];
        char[] t1 = new char[t.length()];

        int len1 = 0;
        int len2 = 0;

        int last = 0;

        while (last < s.length()) {
            if (s.charAt(last) != '#') {
                s1[len1] = s.charAt(last);
                len1++;
            } else {
                len1 = Math.max(--len1, 0);
            }
            last++;
        }

        last = 0;
        while (last < t.length()) {
            if (t.charAt(last) != '#') {
                t1[len2] = t.charAt(last);
                len2++;
            } else {
                len2 = Math.max(--len2, 0);
            }
            last++;
        }

        if (len1 != len2) {
            return false;
        }

        for (int i = 0; i < len1; i++) {
            if (s1[i] != t1[i]) {
                return false;
            }
        }

        return true;
    }
}

//优雅的双指针
class Solution {
    public boolean backspaceCompare(String s, String t) {
        int right1 = s.length() - 1; int right2 = t.length() - 1;
        int skip1 = 0; int skip2 = 0;
        //保证两个都会遍历完
        while (right1 >= 0 || right2 >= 0) {
            //这个循环是处理掉#和skip,保证跳过了指针前面的所有的回退
            while (right1 >= 0) {
                if (s.charAt(right1) == '#') {
                    ++skip1;
                    --right1;
                } else if (skip1 > 0) {
                    --skip1;
                    --right1;
                } else {
                    break;
                }
            }

            while (right2 >= 0) {
                if (t.charAt(right2) == '#') {
                    ++skip2;
                    --right2;
                } else if (skip2 > 0) {
                    --skip2;
                    --right2;
                } else {
                    break;
                }
            }

            //都合法的话,需要比较是否相等
            if (right1 >= 0 && right2 >= 0) {
                if (s.charAt(right1) != t.charAt(right2)) {
                    return false;
                }
            //只要有一个没有遍历完的话,必是错误的,因为上面已经处理掉所有的#和back掉的字符了.
            } else if (right1 >= 0 || right2 >= 0) {
                return false;
            }
            --right1;
            --right2;
        }

        return true;
    }
}

image-20230603221601631

有序数组的平方

977. 有序数组的平方

思路:这其实又是一道经典的双指针的问题,作为一个有序数列,那么数的平方,当然是两边最大了,那么就是从两边,取出一个最大的,放入新数组的最后一个位置即可了。

class Solution {
    public int[] sortedSquares(int[] nums) {
        int len = nums.length;
        int[] res = new int[len];

        //这个是维护结果数组的下标
        int index = len - 1;
        //左
        int left = 0;
        //右
        int right = len - 1;
        //相撞,直接填进去
        while (left <= right) {
            //左平方
            int a = nums[left] * nums[left];
            //右平凡
            int b = nums[right] * nums[right];
            //右大就插尾
            if (a < b) {
                res[index--] = b;
                right--;
            } else {
                res[index--] = a;
                left++;
            }
        }
        return res;
    }
}

长度最小的子数组

209. 长度最小的子数组

思路1:暴力,当然是全部循环遍历一次,找到符合条件的即可,但是因为会超时,所以可以尝试怎么去优化一下判断的条件,让他通过呢?可以当遍历到最后的数据的长度,比你的minLen还短的时候,就可以不用遍历了,因为此时不存在比minLen还短的长度了

思路2:滑动窗口:主要还是一个个双指针的思想,一个end指针,首先去找到符合的数组,然后一个start指针,往后走,去缩短这个数组。而效果看起来就像一个窗口,在中间滑动,然后不停的缩放,故名滑动窗口。

// 暴力
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int len = nums.length;

        int minLen = Integer.MAX_VALUE;
        for (int i = 0; i < len; i++) {
            int sum = 0;
            for (int j = i; j < len && j - i < minLen; j++) {
                sum += nums[j];
                if (sum >= target) {
                    minLen = Math.min(minLen, j - i + 1);
                    break;
                }
            }
        }

        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }
}

//滑动窗口
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int minLen = Integer.MAX_VALUE;
        int len = nums.length;
        int start = 0;
        int sum = 0;
        // 以start为起点,end往后面走,找到符合的,然后start往后面缩,找到最短的
        for (int end = 0; end < len; end++) {
            sum += nums[end];
            //注意这里是while,sum减去值后,可能sum还是大于等于target,所以要继续往下走
            while (sum >= target) {
                sum -= nums[start];
                minLen = Math.min(minLen, end - start + 1);
                start++;
            }
        }
        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }
}

对于滑动窗口,需要分析一下时间复杂度,先说结论,时间复杂度为O(n),为什么里面有两个循环而只是一个O(n)的复杂度呢?可以考虑一下多种情况,end指针,走一格,start指针走一格,那while在里面只是会执行一次,也就是end走了n,start走了n,也就是2n;或者另一种情况,end走到最末尾,start一次性从头走到最后,那也是一个end走了n,start走了n,最后都是O(n)。

螺旋矩阵

59. 螺旋矩阵 II

思路:这种题目,就不是靠算法了,就是考你的思路,逻辑,和对于一些边界的处理,这类题目标签叫做——模拟,也就是,模拟出这道题运作的逻辑。

对于这道题,我们首先要观察,找出通用的作动规律。对于走一圈,可以分为,从左往右,从上往下,从右往左,从下往上,而且需要找到通用的起始点和终止点,我们可以采用一个左闭右开的原则,就可以模拟出通用的逻辑了。一圈的逻辑完成了,接下来就是找到下一圈,每一个的起始和终点了,那么这道题,也就基本迎刃而解了。

//暴力的模拟,直接初始化四个起点,然后每条边走完,找到下一个的起点. 挺好理解的
class Solution {
    public int[][] generateMatrix(int n) {

        if (n == 1) {
            return new int[][]{{1}};
        }
        int r1 = 0; int c1 = 0;
        int r2 = 0; int c2 = n - 1;
        int r3 = n - 1; int c3 = n - 1;
        int r4 = n - 1; int c4 = 0;

        int offset = 1;

        int loop = n * n;

        int cnt = 1;

        int[][] res = new int[n][n];
        while (cnt <= loop) {

            boolean flag = false;
            for (int c = c1; c < n - offset; c++) {
                flag = true;
                res[r1][c] = cnt++;
            }

            //这个是判断,当n为奇数的时候,中间剩下的最后一个点没有被填值,这里做特殊处理.
            if (!flag) {
                res[r1][c1] = cnt++;
            }
            r1++;
            c1++;
            for (int r = r2; r < n - offset; r++) {
                res[r][c2] = cnt++;
            }
            r2++;
            c2--;
            for (int c = c3; c >= offset; c--) {
                res[r3][c] = cnt++;
            }
            r3--;
            c3--;
            for (int r = r4; r >= offset; r--) {
                res[r][c4] = cnt++;
            }
            r4--;
            c4++;
            offset++;
        }

        return res;
    }
}

//优化,只需要维护一个点的移动即可
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res = new int[n][n];
        int top = 0;
        int right = n - 1;
        int bot = n - 1;
        int left = 0;

        int cnt = 1;
        int i ;
        while (cnt <= n * n / 2 * 2) {
            for (i = left; i < right; i++, cnt++) res[top][i] = cnt;
            for (i = top; i < bot; i++, cnt++) res[i][right] = cnt;
            for (i = right; i > left; i--, cnt++) res[bot][i] = cnt;
            for (i = bot; i > top; i--, cnt++) res[i][left] = cnt;
            top++;
            right--;
            bot--;
            left++;
        }

        if (n % 2 == 1) {
            res[top][left] = cnt;
        }
        return res;
    }
}

//还有更优化的,这个做法是真的真的优雅, 通过四个变量,规定了边界,避免了最后的奇数特判。而且代码甚是简洁。
//只不过不再是左闭右开的规则,他的规则则是比较乱的,但是可以通过自己画图,看到还是可行的,
//以n=4为例,上面走4格,右边走3格,下面走3格,左边走2格,因为上面多走了一格的缘故,所以当奇数的时候,最后上面的那一个会走到.所以不用特判.
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res = new int[n][n];
        int top = 0; int left = 0; int right = n - 1; int bottom = n - 1;
        int cnt = 1;
        while (cnt <= n * n) {
            for (int i = left; i <= right; ++i) res[top][i] = cnt++;
            ++top;
            for (int i = top; i <= bottom; ++i) res[i][right] = cnt++;
            --right;
            for (int i = right; i >= left; --i) res[bottom][i] = cnt++;
            --bottom;
            for (int i = bottom; i >= top; --i) res[i][left] = cnt++;
            ++left;
        }
        return res;
    }
}

image-20230603231336787

54. 螺旋矩阵

思路:思路和上面的差不多,就是多加了一个判断的条件,可以模拟一下,在第二个测试样例中的最后的情况,就可以发现需要多加一个判断条件了

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;

        List<Integer> list = new ArrayList<>();

        int top = 0;
        int right = n - 1;
        int bot = m - 1;
        int left = 0;
        int cnt = 1;

        while (cnt <= m * n) {
            for (int i = left; i <= right && cnt <= m * n; ++i, ++cnt) list.add(matrix[top][i]);    
            ++top;
            for (int i = top; i <= bot && cnt <= m * n; ++i, ++cnt) list.add(matrix[i][right]);    
            --right;
            for (int i = right; i >= left && cnt <= m * n; --i, ++cnt) list.add(matrix[bot][i]);    
            --bot;
            for (int i = bot; i >= top && cnt <= m * n; --i, ++cnt) list.add(matrix[i][left]);    
            ++left;
        }
        return list;
    }
}

链表

对于链表,这里直接贴数据结构的源代码

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

移除链表元素

203. 移除链表元素

思路:这道题很简单,但是不像我们平时接触的有一个空的头结点,所以我们需要自己声明一个空的头结点,然后指向给我们的head,然后我们只需要遍历,看这个指针的下一个是否为val,是的话直接越过即可。

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //需要先声明一个空的头结点
        ListNode headNode = new ListNode(0, head);
        //声明一个遍历指针
        ListNode pre = headNode;
        while (pre.next != null) {
            //如果指针的下一个,为val
            if (pre.next.val == val) {
                //跳过
                pre.next = pre.next.next;
            } else {
                pre = pre.next;
            }
        }
        return headNode.next;
    }
}

设计链表

707. 设计链表

思路:这道题,就是纯纯考你的代码能力了,其他都不难的,但是要debug的时候,还是会比较困难,debug我还是用的idea进行debug。然后的话,最好是要单链表和双链表都要会,然后我们自定义代码的时候,一般最好都要使用一个空的头结点,这个是关键!

class MyLinkedList {
    int size;
    ListNode head;
    ListNode tail;

    class ListNode {
        ListNode next;
        ListNode pre;
        int val;

        public ListNode (int val) {
            this.val = val;
            this.next = null;
            this.pre = null;
        }

        public ListNode() {
            this.val = 0;
            this.next = null;
            this.pre = null;
        }
    }

    public MyLinkedList() {
        this.size = 0;
        this.head = new ListNode();
        this.tail = new ListNode();
        //注意这个初始化
        this.head.next = this.tail;
        this.tail.pre = this.head;
    }

    public int get(int index) {
        if (index < 0 || index >= this.size) {
            return -1;
        }

        ListNode p;
        //决定是从前面遍历还是从后面开始遍历
        if (this.size / 2 > index) {
            p = this.head;
            for (int i = 0;i <= index; ++i) {
                p = p.next;
            }
            return p.val;
        } else {
            p = this.tail;
            for (int i = 0;i < this.size - index; ++i) {
                p = p.pre;
            }
            return p.val;
        }
    }

    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    public void addAtTail(int val) {
        addAtIndex(this.size, val);
    }

    public void addAtIndex(int index, int val) {
        if (index < 0 || index > this.size) {
            return;
        }

        ListNode p;

        // 从头开始
        if (this.size / 2 > index) {
            p = this.head;
            for (int i = 0;i < index; ++i) {
                p = p.next;
            }
            ListNode newNode = new ListNode(val);
            newNode.pre = p;
            newNode.next = p.next;
            p.next.pre = newNode;
            p.next = newNode;
        }
        //尾巴开始
        else {
            p = this.tail;
            for (int i = 0; i < this.size - index; ++i) {
                p = p.pre;
            }
            ListNode newNode = new ListNode(val);
            newNode.next = p;
            newNode.pre = p.pre;
            p.pre= newNode;
            newNode.pre.next = newNode;
        }
        ++this.size;
    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= this.size) {
            return;
        }

        ListNode p;

        // 从头开始
        if (this.size / 2 > index) {
            p = this.head;
            for (int i = 0;i < index; ++i) {
                p = p.next;
            }
            p.next = p.next.next;
            p.next.pre = p;
        }
        //尾巴开始
        else {
            p = this.tail;
            for (int i = 0; i < this.size - index; ++i) {
                p = p.pre;
            }
            p.pre.next = p.next;
            p.next.pre = p.pre;
        }
        --this.size;
    }

    public void print() {
        ListNode p = this.head;
        for (int i = 0;i < this.size;++i) {
            p = p.next;
            System.out.println(p.val);
        }
    }

}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

反转链表

206. 反转链表

思路一头插法,先维护一个空的头结点,然后遍历每一个结点,从头结点和头结点的下一个指向的位置中插入即可完成反转。

image-20230521001219654

image-20230521001334110

//思路一
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode newHead = new ListNode();
        newHead.next = null;
        ListNode p = head;
        ListNode next;
        while (p != null) {
            next = p.next;
            p.next = newHead.next;
            newHead.next = p;
            p = next;
        }
        return newHead.next;
    }
}

思路二:我们可以很轻松的利用,实现一个反转的效果。先进后出的效果,刚好就是反转。

class Solution {
    public ListNode reverseList(ListNode head) {
        //提前结束,避免声明栈的空间
        if (head == null) {
            return null;
        }
        if (head.next == null) {
            return head;
        }

        Stack<ListNode> stack = new Stack<>();
        ListNode p = head;
        //依次进栈
        while (p != null) {
            stack.push(p);
            p = p.next;
        }

        //声明空的头结点
       ListNode newHead = new ListNode();
       p = newHead;
       //依次出栈
        while (!stack.isEmpty()) {
            ListNode q = stack.pop();
            p.next = q;
            p = p.next;
        }
        //注意这个点,最后一个结点的next是指向第二个的,需要置空
        p.next = null;
        return newHead.next;
    }
}

思路三:可以借鉴一下数组原地反转的思路,利用两个指针,进行反转。而对于链表的反转,也就是next的指向的问题,那么也可以利用两个指针,保留下一个,然后当前指向前一个即可。

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode p = head;
        while (p != null) {
            //保留下一个的位置
            ListNode next = p.next;
            //反转
            p.next = pre;
            pre = p;
            p = next;
        }
        return pre;
    }
}

思路四:有迭代的双指针,那么递归也可以尝试一下,一样的思路,保留一个当前,一个下一个,一个上一个即可完成该次反转,只不过变的是初始化。

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }
    private ListNode reverse(ListNode pre, ListNode cur) {
        if (cur == null) {
            return pre;
        } else {
            ListNode next = cur.next;
            cur.next = pre;
            return reverse(cur, next);
        }
    }
}

两两交换链表中的节点

24. 两两交换链表中的节点

思路:看到交换,而且原地,很容易就会联想到双指针,如果在数组中,这道题也算是很常规,直接在链表中换 了一种交换的方式

//直接来的第一版代码,就是按照想着想着,然后就做出来的
class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }        
        //头结点
        ListNode newHead = new ListNode(0, head);
        //只需要有需要交换的两个节点的前面两个即可完成本次交换
        ListNode pre = newHead;
        ListNode cur = head;
        while (cur != null) {
            //上一个结点跳过到后面一个结点
            pre.next = cur.next;
            //当前结点跳过到后面一个结点
            cur.next = pre.next.next;
           //回头
            pre.next.next = cur;
            if (cur.next == null) {
                break;
            } else {
                pre = cur;
                cur = cur.next;
            }
        }
        return newHead.next;
    }
}

//优化代码,很简单,while循环里面多了一个判断条件,只需要把里面一些抽取出来即可
class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }        
        //头结点
        ListNode newHead = new ListNode(0, head);
        //只需要有需要交换的两个节点的前面两个即可完成本次交换
        ListNode pre = newHead;
        ListNode cur = head;
        while (cur != null && cur.next != null) {
            //跳过到后面一个结点    
            pre.next = cur.next;
            //当前结点跳过到后面一个结点
            cur.next = pre.next.next;
            //回头
            pre.next.next = cur;
            //步进
            pre = cur;
            cur = cur.next;
        }
        return newHead.next;
    }
}

删除链表的倒数第N个节点

19. 删除链表的倒数第 N 个结点

思路:链表,如果没有维护头结点的长度的话,按常规来做,需要先找到长度,然后才可以去找到倒数的位置。但是,我们可以利用递归的特性,在归的过程中,记录归的次数,也就是刚好是倒数n个结点,然后进行删除即可。

//第一版代码
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //需要一个头结点,防止直接删除完为空
        ListNode newHead = new ListNode(0, head);
        remove(newHead, head, n);
        return newHead.next;
    }
    //使用全局变量,记录次数,也可以声明为一个Integer对象,那么就可以直接传递
    int cnt = 0;
    private void remove(ListNode pre, ListNode cur, int n) {
        if (cur == null) {
            return;
        } else {
            //递,
            remove(cur, cur.next, n);
            //归的时候记录次数
            ++cnt;
            //如果到了次数,直接删除.
            if (cnt == n) {
                pre.next = cur.next;
            }
        }
    }
}

//看了别人的题解,原来这叫做回溯法,可以看到,该递归函数不返回任何东西,那么我们就可以利用他来维护归的次数.
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode newHead = new ListNode(0, head);
        remove(newHead, head, n);
        return newHead.next;
    }
    private int remove(ListNode pre, ListNode cur, int n) {
        if (cur == null) {
            return 0;
        } else {
            int num = remove(cur, cur.next, n) + 1;
            if (num == n) {
                pre.next = cur.next;
            }
            return num;
        }
    }
}

思路二:这里还是贴一下跟常规差不多的思路,利用栈去解决这个问题。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if (head == null || n == 0) {
            return null;
        }
        ListNode newHead = new ListNode(0, head);
        Stack<ListNode> stack = new Stack<>();
        ListNode p = newHead;
        while (p != null) {
            stack.push(p);
            p = p.next;
        }
        ListNode last = null;
        for (int i = 0;i < n; ++i) {
            last = stack.pop();
        }

        if (!stack.isEmpty()) {
            ListNode pre = stack.pop();
            pre.next = last.next;
        }
        return newHead.next;
    }   
}

思路三:这个思路,确实没有想到,但是在看了题解,启发了用双指针,固定的间隔后,也就可以很轻松地写出来。主要思路就是,两个指针,固定间隔n,然后当快指针到达最后的时候,慢指针刚好就是指向被删除的结点,而为了删除,我们要让慢指针在被删除结点之前,那么只需要使快指针的下一个结点为空的时候停止即可。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if (head == null || n == 0) {
            return null;
        }
        ListNode dummy = new ListNode(0, head);
        ListNode pre = dummy;
        ListNode cur = head;
        //固定间隔
        for (int i = 1;i < n; ++i) {
            cur = cur.next;
        }
        //注意这里,不是cur!=null,因为是为了让慢指针走少一格,使其到达被删除结点之前一个.
        while (cur.next != null) {
            cur = cur.next;
            pre = pre.next;
        }
        //跳过被删除结点
        pre.next = pre.next.next;
        return dummy.next;
    }
}

链表相交

面试题 02.07. 链表相交

思路一:这道题,刚拿到手,真的就是想都没想,找重复,直接用哈希map做,很快就做出来了

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Map<ListNode, Integer>  hashMap = new HashMap<>();
        ListNode p = headA;
        //遍历
        while (p != null) {
            hashMap.put(p, 1);
            p = p.next;
        }
        p = headB;
        while (p != null) {
            if (hashMap.getOrDefault(p, 0) == 1) {
                return p;
            }
            p = p.next;
        }
        return null;
    }
}

只不过,做是做出来了,就是。。

image-20230522230552833

那一定当然不是最优解了,我们想想能不能在hashmap上继续进行优化。可以发现,如果结点在最开始就存在了,而你却先遍历了一整个链表,然后再遍历另一个链表,那能不能同时遍历呢?

//同时遍历
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Map<ListNode, Integer>  hashMap = new HashMap<>();
        ListNode p = headA;
        ListNode q = headB;
        while (p != null || q != null) {
            //==是地址相等
            if (p != null && (hashMap.getOrDefault(p, 0) == 1 || p == q)) {
                return p;
            } else if (q != null && hashMap.getOrDefault(q, 0) == 1){
                return q;
            }
            if (p != null) {
                hashMap.put(p, 1);
                p = p.next;
            }
            if (q != null) {
                hashMap.put(q, 1);
                q = q.next;
            }
        }
        return null;
    }
}

但是?更慢了!

image-20230522231338793

思路二:在经过提示之后,可以尝试着用双指针进行书写,只不过代码不是很优雅

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //特判
        if (headA == null || headB == null) {
            return null;
        }
        ListNode a = headA;
        ListNode b = headB;
        int len1 = 0;
        int len2 = 0;
        while (a.next != null || b.next != null) {
            //主要是找a和b的长度差值
            if (a.next != null) {
                a = a.next;
                ++len1;
            }
            if (b.next != null) {
                b = b.next;
                ++len2;
            }
        }
        //如果最后一个不相等的话,那么必定没有相交点
        if (a != b) {
            return null;
        }   
        //长度差
        int temp = Math.abs(len1 - len2);
        a = headA;
        b = headB;
        //让长的链表先遍历,直到两个链表逻辑上一样长
        if (len1 > len2) {
            for (int i = 0;i < temp; ++i) {
                a = a.next;
            }
        } else {
            for (int i = 0;i < temp; ++i) {
                b = b.next;
            }
        }
        //然后就可以同时遍历了,找到第一个相等的结点.
        while (a != null) {
            if (a == b) {
                return a;
            } 
            a = a.next;
            b = b.next;
        }
        return null;     
    }
}

思路三:虽然也是双指针,但是官方题解的代码量,巨少,这个得好好研究一下。假设A链表有a个不相交的结点,有c个相交的结点;B有b个不相交的结点,有c个相交的结点,那么A的长度为a+c,B的长度为b+c。如果在A遍历完的时候,把指针指向B的头结点,B的遍历完的时候,把指针指向A的头结点,那A当移动a+c+b的时候,B移动b+c+a的时候,刚好指针重合,而且还刚好就是相交的点。为什么刚好是相交的点,例如,拿A来说,走完a+c的时候,指向B的头,那么是不是就等价为,最开始的时候,从B开始走,然后走b个结点的时候,就刚好到相交结点。

如果不相交呢,那么走到最后,A和B的指针都会指向null,直接返回null即可了。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode a = headA;
        ListNode b= headB;
        while (a != b) {
            a = a == null ? headB : a.next;
            b = b == null ? headA : b.next;
        }
        return a;
    }
}

只能总结,真的帅!

环形链表II

142. 环形链表 II

这道题主要是要找环的入口,其实可以认为是一道数学题,这题分为两步,一步是判断是否是环,一步是找到环的入口。判断是否是环很简单,只需要快慢指针即可,主要是分析环的入口。

image-20230523210941703

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

那这就简单了。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            //相撞
            if (fast == slow) {
                ListNode p1 = head;
                ListNode p2 = fast;
                //步进为1,从头结点和相撞结点同时出发
                while (p1 != p2) {
                    p1 = p1.next;
                    p2 = p2.next;
                }
                return p1;
            }
        }
        return null;
    }
}

这里再解释一下,为什么,慢指针走不完一圈就会和快指针相遇。

假设环的长度为n。当慢指针刚进入环的时候,快指针一定在环的某个位置对吧(快指针走得快,所以一定提早在环中转了),然后又因为快指针步进为2,慢指针步进为1,所以追及速度为1,假设圈的长度为n,那么,以速度为1,走完一圈n,需要n的时间(这个也就是追及时间,追及时间就是两个指针相差的距离),所以在n的时间内,快慢指针一定相遇。

或者这样讲,两个指针之间的距离最长就是圈的距离,所以相遇的时间也就是圈的距离,所以慢指针最晚也就是刚好未出圈。

哈希表

有效的字母异位词

242. 有效的字母异位词

思路:一样,找到重复的元素,很自然的一个想法就是使用哈希表,对s进行遍历插入,对t进行遍历删除哈希表,最后查看一下哈希表中是否有元素即可

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        Map<Character, Integer> map = new HashMap<>(s.length());
        for (int i = 0; i < s.length(); ++i) {
            char a = s.charAt(i);
            char b = t.charAt(i);
            int cnt1 = map.getOrDefault(a, 0);
            if (cnt1 == -1) {
                map.remove(a);
            } else {
                map.put(a, cnt1 + 1);
            }
            int cnt2 = map.getOrDefault(b, 0);
            if (cnt2 == 1) {
                map.remove(b);
            } else {
                map.put(b, cnt2 - 1);
            }
        }
        return map.values().size() == 0 ? true : false;
    }
}

很简单,直接过!就是有点抽象,这还能有什么改进的方法吗,已经从两次分别遍历,改到最后一次遍历两个字符串了,还是这么慢。

image-20230523222109768

思路二:其实这题还可以使用更抽象的方式解答,直接对两个字符串的值进行排序,然后同时遍历判断是否相等即可。虽然很暴力,但是代码很优雅。

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        char[] array1 = s.toCharArray();
        char[] array2 = t.toCharArray();
        Arrays.sort(array1);
        Arrays.sort(array2);
        return Arrays.equals(array1, array2);
    }
}

思路三:其实这种题,不是对象的重复,就不要无脑使用哈希表了,多使用一下桶排的思想,就是哈希表的原始模式,直接利用数组进行存储。

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() > t.length()) {
            return isAnagram(t, s);
        }
        int[] map = new int[26];
        for (char c: s.toCharArray()) {
            ++map[c - 'a'];
        }
        for (char c: t.toCharArray()) {
            if (map[c - 'a'] <= 0) {
                return false;
            }
            --map[c - 'a'];
        }
        return true;
    }
}

image-20230527221652500

49. 字母异位词分组

思路一:暴力解法,有了上面一题的判断是否为异位词,那么这题就很基本了,暴力很简单,直接两层for循环直接搞定,只不过可以通过一些手段,可以提前退出循环,提高一下效率。

//两层for循环,如何使已经被判断成功的字符串不继续进行判断,直接用桶排即可.
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        int len = strs.length;
        int[] flag = new int[len];
        List<List<String>> res = new ArrayList<>();
        for (int i = 0; i < len; ++i) {
            List<String> list = new ArrayList<>();
            //如果这个字符串已经是某个的异位词了,就不需要进行判断
            if (flag[i] == 1) {
                continue;
            }
            for (int j = i + 1; j < len; ++j) {
                //如果这个字符串已经是某个的异位词了,就不需要进行判断
                if (flag[j] == 1) {
                    continue;
                }
                if (isOk(strs[i], strs[j])) {
                    list.add(strs[j]);
                    flag[j] = 1;
                }
            }
            list.add(strs[i]);
            flag[i] = 1;
            res.add(list);
        }
        return res;
    }

    /**判断是否是异位词*/
    private boolean isOk(String str1, String str2) {
        //长度不一致,直接否
        if (str1.length() != str2.length()) {
            return false;
        }
        //注意都是小写字母
        int[] cnt = new int[26];
        for (int i = 0; i < str1.length(); ++i) {
            ++cnt[str1.charAt(i) - 'a'];
        }

        for (int i = 0; i < str2.length(); ++i) {
            //如果另个一不存在多的这个字母,直接退出
            if (cnt[str2.charAt(i) - 'a'] == 0) {
                return false;
            }
            --cnt[str2.charAt(i) - 'a'];
        }
        return true;
    }
}

暴力解法,所以结果也是很一般的。

image-20230524003819086

思路二:上一题,判断异位词,还有另外一种办法,就是排序,几个异位词的排序之后的结果都是一样的,而且你再观察最后的结果,每一个相同的异位词(即排序之后结果相同的为一组),那是不是我们可以用哈希map,直接key为排序结果,value为多个异位词呢?

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        int len = strs.length;
        Map<String, List<String>> map = new HashMap<>(len);
        //直接一次遍历即可
        for (String str: strs) {
            //排序
            char[] c = str.toCharArray();
            Arrays.sort(c);
            //取出排序的key
            String s = new String(c);
            List<String> list = map.getOrDefault(s, new ArrayList<>());
            //添加当前异位词
            list.add(str);
            map.put(s, list);
        }
        //collection转为list.
        return new ArrayList<>(map.values());
    }
}

image-20230524003841857

383. 赎金信

思路:可以观察到,他也是一个小写字母,这里我们尝试先用着桶排做一下。

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        if (magazine.length() < ransomNote.length()) {
            return false;
        }
        int[] cnt = new int[26];
        for (int i = 0; i < magazine.length(); ++i) {
            ++cnt[magazine.charAt(i) - 'a'];
        }
        for (int i = 0; i < ransomNote.length(); ++i) {
            if (cnt[ransomNote.charAt(i) - 'a'] <= 0) {
                return false;
            }
            --cnt[ransomNote.charAt(i) - 'a'];
        }
        return true;
    }
}

可以,没问题。

这里再尝试一下使用hashmap,处理最通用的该类题目。

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        if (magazine.length() < ransomNote.length()) {
            return false;
        }   
        Map<Character, Integer> map = new HashMap<>(magazine.length());

        for (int i = 0; i < magazine.length(); ++i) {
            map.put(magazine.charAt(i), map.getOrDefault(magazine.charAt(i), 0) + 1);
        }

        for (int i = 0; i < ransomNote.length(); ++i) {
            if (map.getOrDefault(ransomNote.charAt(i), 0) <= 0) {
                return false;
            }
            map.put(ransomNote.charAt(i), map.getOrDefault(ransomNote.charAt(i), 0) - 1);
        }
        return true;
    }
}

438. 找到字符串中所有字母异位词

思路:有了前面一些题目的铺垫,这题感觉也是差不多的思路,只不过加上了一个固定的窗口,进行滑动,代码可以很轻松地写出来。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {

        int len = p.length();
        if (s.length() < len) {
            return Collections.emptyList();
        }
        int start = 0;
        int end = len;
        List<Integer> list = new ArrayList<>();
        //字符数组
        char[] cp = p.toCharArray();
        //异位词比较,直接用排序
        Arrays.sort(cp);
        //固定窗口
        for (; end <= s.length(); ++end, ++start) {
            //截取
            String q = s.substring(start, end);
            //排序
            char[] ca = q.toCharArray();
            Arrays.sort(ca);
            //比较
            if (Arrays.equals(ca, cp)) {
                list.add(start);
            }
        }
        return list;
    }
}

image-20230524224807120

能过是能过,就是有点抽象,所以想想有没有什么改进的办法。看了一下标签,发现用的其实差不多的技术,只不过不是哈希表罢了。

image-20230524224905631

改一下,使用哈希表进行异位词的判断。因为是小写字母,所以还是优先使用桶排

class Solution {
    public List<Integer> findAnagrams(String s, String p) {

        int len = p.length();
        if (s.length() < len) {
            return Collections.emptyList();
        }
        int start = 0;
        int end = len;
        List<Integer> list = new ArrayList<>();
        int[] cnt = new int[26];
        for (int i = 0; i < len; ++i) {
            ++cnt[p.charAt(i) - 'a'];
        }
        for (; end <= s.length(); ++end, ++start) {
            //截取
            String q = s.substring(start, end);
            if (isOk(q, cnt)) {
                list.add(start);
            }
        }
        return list;
    }

    private boolean isOk(String str, int[] res) {
        int[] cnt = Arrays.copyOf(res, 26);
        for (int i = 0; i < str.length(); ++i) {
            if (cnt[str.charAt(i) - 'a'] <= 0) {
                return false;
            }
            --cnt[str.charAt(i) - 'a'];
        }
        return true;
    }
}

感觉还是没有什么质的飞跃,主要问题是,一直出现一个重复的数组复制,但是想不到什么解决思路,还是得看看题解。

image-20230524225816645

看了题解,恍然大悟,他的思路不是去对一个数组进行维护,而是维护两个数组,一个是p的,一个是滑动窗口中的。而且数组没必要一直复制,对于滑动窗口,一直在向前走,中间的几个其实没有变化,变化的是头尾字符串中字符的数量,那只要把字符串的头的数量--,尾巴++即可可以直接维护该数组。

速度把之前的代码改了一下,最后的边界问题,直接复制一遍代码,立马过了先。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int len = p.length();
        if (s.length() < len) {
            return Collections.emptyList();
        }
        int start = 0;
        int end = len;
        List<Integer> list = new ArrayList<>();
        //最终的异位词的字母统计
        int[] pcnt = new int[26];
        //目标词的字母统计
        int[] scnt = new int[26];
        //初始化,从第一个与p相同长度的字符串开始
        for (int i = 0;i < len; ++i) {
            ++pcnt[p.charAt(i) - 'a'];
            ++scnt[s.charAt(i) - 'a'];
        }
        //start为目标字符串的开始,end为结束的下一个
        for (; end < s.length(); ++end, ++start) {
            //判断初始化的第一个是否相等和后面的是否相等
            if (Arrays.equals(pcnt, scnt)) {
                list.add(start);
            }
            //滑动窗口移动之前,先把上一个字符串的第一个的字母--,
            --scnt[s.charAt(start) - 'a'];
            //滑动窗口移动之后的最后的位置就是end(end为当前字符串结束的下一个)
            //++下一个字符串的最后一个字母
            ++scnt[s.charAt(end) - 'a'];
        }
        //可以看到,当前条件,最后一个相同长度的字符串,没办法在循环中判断,所以额外添加条件
        if (Arrays.equals(pcnt, scnt)) {
            list.add(start);
        }
        return list;
    }
}

image-20230524235310828

还是有挺大的优化的,但是该代码还是不够优雅,这里再看看评论区里面的大佬的代码,真的优雅极了,把滑动窗口做到了极致。

还记得前面那道209. 长度最小的子数组吗,最典型的滑动窗口,双指针,然后前后指针同时去维护一个东西,那道题是数字和。而这道题,就是一个哈希表了。和上面的思路差不多,因为哈希表(数组)中间其实没有变化,只要头维护哈希表,尾巴也维护哈希表,那么就可以实现了,这时候,只要滑动窗口的长度刚好等于p的长度,那么就是了。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        //提前退出
        if (p.length() > s.length()) {
            return Collections.emptyList();
        }
        //快慢指针
        int low = 0;
        int high = 0;
        //桶
        int[] cnt = new int[26];
        //初始化桶
        for (int i = 0; i < p.length(); ++i) {
            ++cnt[p.charAt(i) - 'a'];
        }   
        List<Integer> list = new ArrayList<>();
        //滑动窗口退出
        while (high < s.length()) {
            //如果当前字符串的头,存在p中
            if (cnt[s.charAt(high) - 'a'] > 0) {
                //p的桶对该字母--;
                --cnt[s.charAt(high++) - 'a'];
                //如果窗口长度刚好与p相等,则就是异位词,注意这里low不++,是因为,当你low++之前,需要把cnt[low]++,这和下面的else条件是一样的,因为当是异位词的时候,cnt[high]一定失败,走进else.
                if (high - low == p.length()) list.add(low);
            } 
            //如果当前字符串的头,不存在p中,那么滑动窗口移动,后沿前移,然后把桶还原.
            else {
                ++cnt[s.charAt(low++) - 'a'];
            }
        }
        return list;
    }
}

这个思路,也只能说是优雅!

image-20230525003956519

两个数组的交集

349. 两个数组的交集

思路:交集,也就是判断一个重复的元素,那直接用哈希set即可。一个存,一个遍历判断,如果有重复,就直接添加到set集合里面。

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        //保证前面一个短
        if (nums1.length > nums2.length) {
            return intersection(nums2, nums1);
        }
        Set<Integer> set = new HashSet<>();
        Set<Integer> res = new HashSet<>();
        for (int i: nums1) {
            set.add(i);
        }
        for (int i: nums2) {
            if (set.contains(i)) {
                res.add(i);
            }
        }
        return res.stream().mapToInt(Integer::intValue).toArray();
    }
}

很顺利地过了,只不过,我没有想到会这么慢。

image-20230525165848014

然后,我把stream流的那部分,换成了一个手写的遍历填充int数组,性能直接大幅度提升。

image-20230525205229596

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        //保证前面一个短
        if (nums1.length > nums2.length) {
            return intersection(nums2, nums1);
        }
        Set<Integer> set = new HashSet<>();
        Set<Integer> res = new HashSet<>();
        for (int i: nums1) {
            set.add(i);
        }
        for (int i: nums2) {
            if (set.contains(i)) {
                res.add(i);
            }
        }
        int[] resa = new int[res.size()];
        int cnt = 0;
        for (int i: res) {
            resa[cnt++] = i;
        }
        return resa;
    }
}

这性能的差距,主要体现在其中的自动装箱和自动拆箱上。

350. 两个数组的交集 II

思路:这题就尝试用一下双指针的思路来做了,也是一样很简单,就两个数组,分别有一个指针,然后同时遍历,直到相等的就添加进去。

class Solution {
    public int[] intersect(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            return intersect(nums2, nums1);
        }
        Arrays.sort(nums1);
        Arrays.sort(nums2);
        int index1 = 0;
        int index2 = 0;
        int[] res = new int[1000];
        int cnt = 0;
        while (index1 < nums1.length && index2 < nums2.length) {
            if (nums1[index1] == nums2[index2]) {
                res[cnt++] = nums1[index1];
                ++index1;
                ++index2;
            } else if (nums1[index1] < nums2[index2]){
                ++index1;
            } else {
                ++index2;
            }
        }
        return Arrays.copyOfRange(res, 0, cnt);
    }   
}

image-20230525212811520

快乐数

202. 快乐数

思路:这题主要就是判断什么时候需要继续走循环,什么时候可以成功,成功并退出的条件很简单,只要为1的时候,但是什么时候需要判断失败了呢,其实这道题,我也不知道要怎么写,但是就是当时做题的时候,突然感觉,是不是这样一直分解然后平方和,最终是不是会落到一个循环之中,然后我就使用了一个hashSet集合,去存放,如果当重复了就直接失败退出即可,然后到1,就成功退出,写完后,提交,居然就过了。

对于为什么最后会走近循环里面,我也不是很清楚,只是感觉这种题目,一般都是会有一个规律,所以只是当时做了一个尝试罢了。

class Solution {
    public boolean isHappy(int n) {
        int sum = n;
        Set<Integer> set = new HashSet<Integer>();
        while (true) {
            n = sum;
            //初始化
            sum = 0;
            //遍历每一位,然后平方+sum
            while (n > 0) {
                int dif = n % 10;
                sum += dif * dif;
                n /= 10;
            }
            //只要hashset中存在过这个sum,那么一定会走无限循环
            if (set.contains(sum)) {
                return false;
            } 
            //只要为1,那就可以退出
            else if (sum == 1) {
                return true;
            }
            set.add(sum);
        }
    }
}

image-20230526171000051

那么接下来就需要看一下题解,去分析一下最后为什么会循环重复了。对于这部分,我选择引用一下官方的题解,官方的题解已经解释得很透彻了。

image-20230526171617099

即就是说,当9999999999的时候,每一位的平方和,也只是达到了4位数1053,而4位数最大的9999的平方和也都变为了三位数324,所以1053的平方和也最多只能达到3位数,而最大的三位数的平方和,最大也只能达到243,那么,观察规律,无论什么数,最后应该都会落到3位数的平方和,所以位数会越来越少,而且会限制在243以内。所以,无限次循环下,他一定会出现重复,而重复,那么就表示一个循环,所以也就不可能为最后变成1。

这里把代码优化一下,没有什么变化,只是把代码变优雅一点。

class Solution {
    public boolean isHappy(int n) {
        Set<Integer> set = new HashSet<>();
        int i = n;
        while (i != 1 && !set.contains(i)) {
            set.add(i);
            i = getNext(i);
        }
        return i == 1;
    }

    private int getNext(int n) {
        int sum = 0;
        while (n > 0) {
            int i = n % 10;
            sum += i * i;
            n /= 10;
        }
        return sum;
    }
}

两数之和

1. 两数之和

思路:作为力扣第一题,可能已经难倒了一片人,很直观的思路,直接暴力枚举,两层for循环,类似选择排序一样的两个指针进行遍历,然后找到对应答案即可。但是,有没有办法保存前面遍历过的数字的一个索引呢,可以看到,数字的大小很大,所以用数组一定不现实,但是我们可以使用hashmap,key为数组,value为索引,然后我们只要找到target - num的key,就可以直接找到对应的索引,而这样我们只需要一次遍历即可。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; ++i) {
            int res = map.getOrDefault(target - nums[i], -1);
            if (res != -1) {
                return new int[]{i, res};
            }
            map.put(nums[i], i);
        }
        return new int[0];
    }
}

image-20230526205245752

四数相加II

454. 四数相加 II

思路:这种题,我看到有位网友总结得很好。

image-20230526210615633

所以,这道题是A+B+C+D=0,我们要拆分成A+B = -(C+D),所以这样,我们就拆分成两组,然后我们只需要使用hashmap存放A+B的数量,然后遍历C+D找到A+B = -(C+D)即可。

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i: nums1) {
            for (int j: nums2) {
                map.put(i + j, map.getOrDefault(i + j, 0) + 1);
            }
        }
        int cnt = 0;
        for (int i: nums3) {
            for (int j: nums4) {
                cnt += map.getOrDefault(-(i + j), 0);
            }
        }
        return cnt;    
    }
}

image-20230528202203515

15. 三数之和

思路:看了这个名字,我就被固定思维了,还是想着,一个map存key为A+B,然后value存index集合,但是这么写,最后还要一个索引值不相等的条件,然后返回的还是值的,然后还要不重复,所以很乱,而且中间有很多个转换,所以超时了。

然后我就看了一下题解,看到了一个评论。

排序,固定一个数,然后双指针往中间走。

恍然大悟,如果用双指针,这样索引的条件就很容易保证不相等了,然后对于顺序问题,因为第一次排序后,里面已经有序,而相同顺序的list调用equals是ture,所以我们可以使用set集合进行存储,这样同时解决了上面说的所有问题。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        Set<List<Integer>> set = new HashSet<>();
        //固定一个数
        for (int i = 0; i < nums.length; ++i) {
            //下一个
            int left = i + 1;
            //最后一个,往中间走
            int right = nums.length - 1;
            while (left < right) {
                //和
                int sum = nums[i] + nums[left] + nums[right];
                //==0,直接出
                if (sum == 0) {
                    //因为是有序的list,所以不用担心重复问题,直接存入set即可,set会自己调用equals去重
                    List<Integer> list = new ArrayList<>(Arrays.asList(nums[i], nums[left], nums[right]));
                    set.add(list);
                    //往中间走
                    ++left;
                    --right;
                    //下面同理往中间走
                } else if (sum > 0) {
                    --right;
                } else {
                    ++left;
                }
            }
        }
        //转为list
        return new ArrayList<>(set);
    }
}

只不过,不知道为啥这么慢,所以还是得看看题解的思路。

image-20230526221301641

看了官方的题解,感觉都没有我的优雅,所以他应该是做了挺多的细节处理,主要是一个找边界的处理,因为重复的问题,该值成功相加为0(不为0),所以接着重复的也一定为0(不为0),所以只要把几个if换为while即可。不为0的那部分,需要找边界吗,其实不需要,因为在外面的大while已经做了他的工作,而你加了分别加了两个while进行找边界的话,将会产生更多的判断耗时,主要是那个相加成功为0的,因为一旦成功,需要进入条件,然后还要加入set集合中,会有比较多的操作,所以当加了while条件后,就可以把set换为list集合了。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        //优化,把set换为list,通过附加条件来去重
        List<List<Integer>> res = new ArrayList<>();
        int pre = Integer.MAX_VALUE;
        //固定一个数
        for (int i = 0; i < nums.length; ++i) {
            //固定的数出现重复的话,需要跳过,不然list集合会出现重复
            if (nums[i] == pre) {
                continue;
            }
            //如果固定的数大于0,那么排序后后面的值相加不可能==0
            if (nums[i] > 0) {
                break;
            }
            //记录当前的固定数,用于做下一次判断
            pre = nums[i];
            //下一个
            int left = i + 1;
            //最后一个,往中间走
            int right = nums.length - 1;
            int target = -nums[i];
            while (left < right) {
                //==0,直接出
                if (nums[left] + nums[right] == target) {
                    List<Integer> list = new ArrayList<>(Arrays.asList(nums[i], nums[left], nums[right]));
                    res.add(list);
                    //往中间走
                    //需要跳过与当前nums[left]值相同的数,不然会出现结果重复
                    int temp = nums[left];
                    while (left < right && temp == nums[left]) ++left;
                    //需要跳过与当前nums[right]值相同的数,不然会出现结果重复
                    temp = nums[right];
                    while (left < right && temp == nums[right]) --right;
                    //下面同理往中间走
                } else if (nums[left] + nums[right] > target) {
                    --right;
                } else {
                    ++left;
                }
            }
        }
        return res;
    }
}

image-20230526224818636

image-20230526224855418

四数之和

18. 四数之和

思路:做了上面的一题三数之和后,如果这道题还不会做,真的是对不起自己了。和三数之和不同的唯一点就是,三数之和是固定一个数,而四数之和是固定两位数。只不过这道题要注意一下测试样例中的一些数,可能会导致溢出。

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();

        int pre1 = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length - 1; ++i) {
            //剪枝处理,如果前面的为正数,还比target大,那么不可能
            if (nums[i] > target && nums[i] >= 0) {
                break;
            }
            //重复跳过
            if (nums[i] == pre1) continue;
            pre1 = nums[i];
            int pre2 = Integer.MAX_VALUE;
            for (int j = i + 1; j < nums.length; ++j) {
                //剪枝处理,如果前面的为正数,还比target大,那么不可能
                if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) {
                    break;
                }
                //重复跳过
                if (nums[j] == pre2) continue;
                pre2 = nums[j];

                //左右指针
                int left = j + 1; int right = nums.length - 1;
                while (left < right) {
                    //注意使用long,避免溢出
                    long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
                    if (target == sum) {
                        List<Integer> list = new ArrayList<>(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        res.add(list);
                        ++left; --right;
                        while (left < right && nums[left] == nums[left - 1]) ++left;
                        while (left < right && nums[right] == nums[right + 1]) --right;
                    } else if (sum > target) {
                        --right;
                    } else {
                        ++left;
                    }
                 }
            }
        }
        return res;
    }
}

image-20230526234233977

溢出的数据:

image-20230526234310032

image-20230526234326507

字符串

反转字符串

344. 反转字符串

思路:这题,应该是大家大一C语言课程就有学到的吧,很经典的一道双指针的题目,一头一尾相互交换,走到中间就可以停止了,对于奇数个和偶数个的问题,可以自己尝试一下,并探究为什么奇数偶数的中间条件都是len/2。

class Solution {
    public void reverseString(char[] s) {
        for (int i = 0; i < s.length / 2; ++i) {
            char temp = s[i];
            s[i] = s[s.length - i - 1];
            s[s.length - i - 1] = temp;
        }
    }
}

然后对于交换,除了经典利用中间变量,还有一种位运算交换。主要是利用这个公式a^b^a = b;

class Solution {
    public void reverseString(char[] s) {
        for (int i = 0; i < s.length / 2; ++i) {
            s[i] ^= s[s.length - i - 1];
            s[s.length - i - 1] ^= s[i];
            s[i] ^= s[s.length - i - 1];
        }
    }
}

image-20230527210133083

image-20230527210210119

反转字符串II

541. 反转字符串 II

思路:一开始看到这题,感觉条件非常的多,会有点乱,但是,当时用实例1 “abcdefg" 去模拟的时候,你就会发现,他就是反转k个字符,跳过k个字符,反转k个字符,跳过k个字符,最后反转剩下长度的字符。所以当我们找到规律后,就会很简单了,只不过需要注意挺多条件的。

//第一版代码,还有很多丑陋的代码可以优化.
class Solution {
    public String reverseStr(String s, int k) {
        //转换为数组,便于交换
        char[] ca = s.toCharArray();
        //left = n * k,right = 2 * k - 1;
        int left = 0; int right = 2 * k - 1;
        //主要是left在遍历,
        for (; left < s.length(); left = right + 1, right += 2 * k) {
            //这里是判断,是否到达最后剩余的字符串,手动指定r的值,防止r溢出,
            int r ;
            if (left + k >= ca.length) {    
                r = ca.length;
            } else {
                r = left + k;
            }
            //需要转换字符串的长度
            int len = r - left;
            //正常反转,只不过,i和i的条件,需要添加偏移量 left,
            for (int i = left; i < len / 2 + left; ++i, --r) {
                ca[i] ^= ca[r - 1];
                ca[r - 1] ^= ca[i];
                ca[i] ^= ca[r - 1];
            }
        }
        return new String(ca);
    }
}

image-20230527220852085

相对与上面的代码,其实改动不算多,主要是一些条件的简化,和一些冗余的去除。

class Solution {
    public String reverseStr(String s, int k) {
        int slen = s.length();
        //转换为数组,便于交换
        char[] ca = s.toCharArray();
        //left = n * k,right = 2 * k - 1;
        int left = 0; 
        //主要是left在遍历,
        for (; left < slen; left += 2 * k) {
            //长度需要判断是否是剩余的字符串,需要手动调整需要反转字符串的长度.
            int len = left + k >= slen ? slen - left : k;
            for (int i = left; i < len / 2 + left; ++i) {
                 // (len + left)      - (i - left) - 1
                // (right索引+1,即长度) - (偏移量) - 1
                int r = len - i - 1 + 2 * left;
                ca[i] ^= ca[r];
                ca[r] ^= ca[i];
                ca[i] ^= ca[r];
            }
        }
        return new String(ca);
    }
}

替换空格

剑指 Offer 05. 替换空格

思路:很简单吧,直接遍历,然后有空格就换

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

image-20230527220911702

翻转字符串里的单词

151. 反转字符串中的单词

思路:我是做了下面题后,然后再来做这道题的,然后了解到了一个先局部反转,然后再整体反转的思路,所以这道题也就有了思路,只不过第一版代码还是很丑陋,但是很好理解,也就是贯彻了“先局部反转,然后再整体反转的思路”,然后最后加加减减,根据答案一步步修改,也是可以成功ac掉这道题。

class Solution {
    public String reverseWords(String s) {
        //去除实例二的情况.
        s = s.trim();
        StringBuilder sb = new StringBuilder("");
        //用left找到局部反转的开始
        int left = 0;
        //right为局部反转的尾巴+1.
        for (int right = 0; right < s.length(); ++right) {
            //当right==' '
            if (s.charAt(right) == ' ') {
                //那么这个单词的末尾就是right - 1;
                int r = right - 1;
                //从尾部开始拼接
                while (left <= r) {
                    sb.append(s.charAt(r--));
                }
                //加上空格
                sb.append(' ');
                //left找到下一个单词
                left = right;
                while (left < s.length() && s.charAt(left) == ' ') ++left;
                //right=left,开始找该单词的末尾
                right = left;
            }
        }
        //补丁,因为最后一个单词的末尾没有空格.所以需要额外处理.但是我们也可以通过调用trim后,手动补一个空格.
        int r = s.length() - 1;
        while (left <= r) {
            sb.append(s.charAt(r--));
        }
        return sb.reverse().toString();
    }
}

当时缝缝补补出来的代码,没想到能打败这么多。

image-20230528215054977

把代码改成第一版注释中说的,添加一个末尾的空格。

class Solution {
    public String reverseWords(String s) {
        //去除实例二的情况.
        s = s.trim();
        s += " ";
        StringBuilder sb = new StringBuilder("");
        //用left找到局部反转的开始
        int left = 0;
        //right为局部反转的尾巴+1.
        for (int right = 0; right < s.length(); ++right) {
            //当right==' '
            if (s.charAt(right) == ' ') {
                //那么这个单词的末尾就是right - 1;
                int r = right - 1;
                //从尾部开始拼接
                while (left <= r) {
                    sb.append(s.charAt(r--));
                }
                if (right != s.length() - 1) {
                    //加上空格
                    sb.append(' '); 
                }

                //left找到下一个单词
                left = right;
                while (left < s.length() && s.charAt(left) == ' ') ++left;
                //right=left,开始找该单词的末尾
                right = left;
            }
        }
        return sb.reverse().toString();
    }
}

更加快了。

image-20230528215236941

左旋转字符串

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

思路:首先为了先ac掉这道题,先采用暴力一点的方法,申请一下额外的空间。其实左右旋转后,只不过是起始的指针发生了变化,然后再把没有遍历到的数组继续添加到结果数组中罢了。对此,我们只要找到左旋转n个位置后的起始位置,这道题就很轻松了。

class Solution {
    public String reverseLeftWords(String s, int n) {
        int len = s.length();
        //指针旋转后偏移位置
        int p = n % len;
        //额外空间,存放结果数组
        char[] res = new char[len];
        int cnt = 0;
        //从旋转后的第一个开始
        for (int i = p; i < len; ++i) {
            res[cnt++] = s.charAt(i); 
        }
        //补充剩余的
        for (int i = 0; i < p; ++i) {
            res[cnt++] = s.charAt(i);
        }
        return new String(res);
    }
}

image-20230528204950963

然后如果硬要不使用额外空间的话,也不是很难,主要还是找到旋转后的起始点,然后让开头和结尾两部分,同时反转即可,然后再全部进行反转。

class Solution {
    public String reverseLeftWords(String s, int n) {
        int len = s.length();
        //指针旋转后偏移位置
        int p = n % len;
        char[] ca = s.toCharArray();
        //反转前面
        for (int i = 0; i < p / 2; ++i) {
            ca[i] ^= ca[p - i - 1];
            ca[p - i - 1] ^= ca[i];
            ca[i] ^= ca[p - i - 1];
        }
        //反转后面
        for (int i = p; i < (len - p) / 2 + p; ++i) {
            ca[i] ^= ca[len - i - 1 + p];
            ca[len - i - 1 + p] ^= ca[i];
            ca[i] ^= ca[len - i - 1 + p];
        }
        //反转全部
        for (int i = 0; i < len / 2; ++i) {
            ca[i] ^= ca[len - i - 1];
            ca[len - i - 1] ^= ca[i];
            ca[i] ^= ca[len - i - 1];
        }
        return new String(ca);
    }
}

不借助空间的话,需要做三个遍历反转,就是属于是以时间换空间,有点不是很推荐,但是思路还是需要学习的。

image-20230528212213627

看了一下题解,发现我的第一种思路可以进行优化。真的很优雅。

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

实现 strStr()

28. 找出字符串中第一个匹配项的下标

思路:直接暴力两层遍历,一个固定起点,一个遍历单词

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.length() > haystack.length()) {
            return -1;
        }
        for (int i = 0; i < haystack.length(); ++i) {
            //开始遍历单词的起点
            int index = i;
            //index不要越界,index-i为needle的指针不要越界,相等就一直遍历
            while (index < haystack.length() 
            && index - i < needle.length() 
            && haystack.charAt(index) == needle.charAt(index - i)) {
                ++index;
            }
            //直到遍历到最后,那么就表示相等.
            if (index - i == needle.length()) {
                return i;
            }
        }
        return  -1;
    }
}

image-20230528223710676

这个确实是很暴力,也是最简单想出来的,但是实在没有别的思路了,所以得去看一看题解,一看题解,快懵了。

image-20230529091352090

KMP

这里着重介绍一下KMP算法。

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

KMP核心——前缀表

而如何做到记录之前已经匹配的内容,前缀表,也就是代码中常见的next数组或者prefix数组。

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

KMP实例

这里我们使用两组字符串来解释KMP算法。

aabaabaaf

aabaaf

abcdefabcdeg

abcdeg

首先看第一组字符串,正常的时候,就会有两个指针,一个是主串,一个是模板串的指针,当分别遍历到b和f的时候,就会产生冲突,按照正常的暴力算法,那么主串指针会回到开始遍历的下一个位置,而模板串从头开始。那么这就会产生O(m*n)的时间复杂度。

那么KMP算法,他的实际效果是如何呢?

他的主串指针,没有回到开始位置的下一个,而是继续在最后的一个位置;模板串指针,没有从头开始,而是跳到了b的位置,然后继续开始遍历。那么实际的时间复杂度就降到了O(m+n)变为一个线性的时间复杂度。

接下来解释一下为什么主串指针和模板字符串指针为什么可以这么操作,他的原因到底是什么呢?

看第一组字符串,当走到b和f冲突的时候,是不是就是表示,主串和模板串前面相同长度部分是相同的。

aabaa baaf

aabaa f

注意观察,上面字符串的末尾aa,和下面字符串的开头aa相同,是不是表示,当你重新遍历的时候,这两个字符是相等的,并对结果没有任何影响。

那是不是,就表示,我的模板字符串,就可以往后移动,如下所示:

aabaab

aabaaf

那是不是表示,我的两个指针可以都从b开始遍历比较了。那这样,两个指针,就可以避免了很多次无效的遍历,这就是KMP的实际效果。

你现在可能对模板串可以跳过到b位置开始遍历有点理解,但是可能不理解为什么主串的指针可以从上一个位置继续遍历,这里我们用另一组字符串来解释。

abcdefabcdeg

abcdeg

同样,我们遍历分别遍历到f和g的时候发现冲突了,这时候我们就会去寻找是否能跳过一些不必要的比较。同样,我们发现上面字符串的后缀,没有和下面字符串的前缀一样的部分,那表明什么?表明上面字符串到冲突开始部分abcde,除了第一个位置开始,再也没有出现a这个字符!因为a是下面模板字符串最短的一个前缀了,所以,你主串从头开始遍历,是没有意义的,将会一直失败!

这时候,你就可以理解了KMP的核心思想了。接下来就有个问题,我怎么知道模板串要跳到哪个位置呢?不急,接下来是KMP核心部分,前缀表的原理以及实现。

前缀表原理及实现

前缀表,主要作用就是,告诉我们,当匹配失败的时候,可以跳到哪个位置。前缀表,其实也是一个数组,只不过索引下存的值就是最长相等前缀的长度,而索引为长度就可以作为下一个比较的位置。

前缀表的实现步骤:

  1. 初始化
  2. 不相等的时候
  3. 相等的时候
  4. 更新前缀表

前缀和后缀不是回文的关系,而是相等的关系,所以其实实现的想法也不会太难,主要方法是双指针主要是要有这个思想。

    public static int[] getNextArray(String s) {
        // 初始化
        int[] next = new int[s.length()];
        // 第一个为0,因为第一个无前缀.前缀不能包含最后一个,后缀不能包含第一个
        next[0] = 0;
        //慢指针
        int start = 0;
        // i为快指针
        for (int i = 1; i < s.length(); ++i) {
            //注意这里是循环,因为回退不止包含一次,如果一直不相等,需要一直回退,还要注意>0
            while (start > 0 && s.charAt(start) != s.charAt(i)) {
                start = next[start - 1];
            }
            //如果相等,需要更新索引和前缀表
            if (s.charAt(start) == s.charAt(i)) {
                ++start;
                next[i] = start;
            }
        }
        return next;
    }

题目答案

所以,这题我们利用前缀表也可以很快的解出来,唯一注意的就是字符串开始的下标。

class Solution {
    public int strStr(String haystack, String needle) {
        if (haystack.length() < needle.length()) {
            return -1;
        }
        int[] next = getNextArray(needle);
        int start = 0;
        int i;
        for (i = 0; i < haystack.length(); ++i) {
            while (start > 0 && haystack.charAt(i) != needle.charAt(start)) {
                start = next[start - 1];
            }
            if (haystack.charAt(i) == needle.charAt(start)) {
                ++start;
            }
            if (start == needle.length()) {
                return i - start + 1;
            }
        }
        return -1;
    }

    public int[] getNextArray(String s) {
        int[] next = new int[s.length()];
        int start = 0;
        for (int i = 1; i < s.length(); ++i) {
            while (start > 0 && s.charAt(start) != s.charAt(i)) {
                start = next[start - 1];
            }
            if (s.charAt(start) == s.charAt(i)) {
                ++start;
                next[i] = start;
            }
        }
        return next;
    }
}

优化

主要是取出字符串长度,并使用++等操作,避免多次运算。

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(); int m = needle.length();
        if (m == 0 || n < m) {
            return -1;
        }
        int[] next = new int[m];
        int j = 0;
        for (int i = 1; i < m; ++i) {
            while (j > 0 && needle.charAt(j) != needle.charAt(i)) j = next[j - 1];
            if (needle.charAt(j) == needle.charAt(i)) next[i] = ++j;
        }
        j = 0;
        for (int i = 0; i < n; ++i) {
            while (j > 0 && needle.charAt(j) != haystack.charAt(i)) j = next[j - 1];
            if (needle.charAt(j) == haystack.charAt(i)) ++j;
            if (j == m) return i - j + 1;
        }
        return -1;
    }
}

重复的子字符串

459. 重复的子字符串

因为这道题挺不错的,包括暴力解法也是一个挺不错的思想,所以这道题得好好学一下。

思路一:这个暴力,其实做得比下面的KMP还难受,感觉不是很好做出来。有很多细节需要考虑。

  1. 重复的子串的长度,必须是可以整除字符串的长度。
  2. 长度为1的字符串,并没有子串,和我们的子集的概念不一样。

但是要做出这道题,我们可以先把两个条件先忽略掉,然后再来做这道题会比较容易。

主要也是一个双指针的思想,一个是子串末尾的指针,还有一个是前面用来判断是否是重复子串的指针。下面是两个指针的运行情况。

abcabcabc——false

abcabcabc——false

abcabcabc——true

abcabcabc——true

abcabcabc——true

abcabcabc——true

……

有图的展示,应该就很清晰要怎么运行了,然后对于两个条件,可以看看代码中是怎么实现的,这里就不展示出来了。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int len = s.length();
        //从1开始,因为从0开始没有意义,要找到重复字符串,也要从1才能找到,并且为了避免下面的
        //len % i,i为0,还有只有一个长度的字符串的问题.
        for (int i = 1, n = len / 2; i <= n; ++i) {
            //重复子串的长度需要能整除字符串长度
            if (len % i == 0) {
                boolean match = true;
                for (int j = i; j < len; ++j) {
                    // 第一次,j和0开始比较
                    if (s.charAt(j) != s.charAt(j - i)) {
                        match = false;
                        break;
                    }   
                }
                if (match) {
                    return true;
                }
            }
        }
        return false;
    }
}

思路二:KMP算法,就是用来找到字符串中是否有包含重复子字符串的,用在这道题刚刚好,如果你刚写了上面那道题,那么这道题的前缀表,就可以很快的写出来了,但是最后的判断是否有重复的字符串,倒是很让人犯难。

先说结论,如果字符串是由重复子串组成的,那么你重复子串的最小单位,就是你这个字符串里的最长相等前后缀所不包含的那一个子串组成的。

接下来是数学的推导。

ababab

ababab

这时候,最长的相等的前后缀为abab。我们设上字符串的相等前后缀为t,下字符串的相等前后缀为s。

那么可以推出,t0=s0,t1=s1,s2=t0,s3=t1。那么可以推出s0=s2,s1=s3,然后继续往下推,就可以推出,字符串的ababab中,可以分为三部分ab、ab、ab,所以结论中的不包含的最长相等前后缀的子串刚好就是重复的子串,所以,只要剩余不包含的子串的长度,可以整除字符串的长度,就表示他是重复字符串。

所以可以写出这样的代码。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int m = s.length();
        int[] next = new int[m];
        int j = 0;
        for (int i = 1; i < m; ++i) {
            while (j > 0 && s.charAt(j) != s.charAt(i)) j = next[j - 1];
            if (s.charAt(j) == s.charAt(i)) next[i] = ++j;
        }
        //m - (next[m - 1])为不包含的长度, >0表示有相等前后缀
        return next[m - 1] > 0 && m % (m - (next[m - 1] )) == 0;
    }
}

双指针法

移除元素

27. 移除元素

思路:这道题之前做过的,在数组那一节里面,只不过,当时是使用了一个交换的思路,还不够达到最优雅,这里写一版最优雅的。主要是直接覆盖即可,然后right前移,他也就不会遍历到对应的位置了。

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0; int right = nums.length - 1;

        while (left <= right) {
            if (nums[left] == val) {
                nums[left] = nums[right--];
            } else {
                ++left;
            }
        }
        return left;
    }
}

image-20230530224620454

26. 删除有序数组中的重复项

思路 :双指针的思路,一个是维护当前前面都是不重复元素的下标,一个是遍历找到不重复的元素,然后直接覆盖即可。

class Solution {
    public int removeDuplicates(int[] nums) {
        if (nums.length == 1) {
            return 1;
        }
        int left = 0; int right = 1;
        while (right < nums.length) {
            if (nums[left] != nums[right]) {
                nums[left++ + 1] = nums[right];
            }
            ++right;
        }
        return left + 1;
    }
}

image-20230530225547860

283. 移动零

思路:双指针,左指针维护当前非0元素的长度下标,另一个去找到非0元素,去进行交换。

class Solution {
    public void moveZeroes(int[] nums) {
        int left = 0; int right = 0;
        while (right < nums.length) {
            if (nums[right] != 0) {
                int temp = nums[right];
                nums[right] = nums[left];
                nums[left++] = temp;
            } 
            ++right;
        }
    }
}

image-20230530230315114

844. 比较含退格的字符串

思路:如果在面试中遇到这道题,先不要去考虑双指针,先把题目ac掉再说,那就先借助一些额外的空间,然后,这道题的核心点就是,从后往前遍历,因为退格,只会影响到前面的字符串。这里借助了一个指针,还有一个skip的变量,表示要跳过多少个字符。

class Solution {
    public boolean backspaceCompare(String s, String t) {
        s = getStr(s);
        t = getStr(t);
        return s.equals(t);
    }
    private String getStr(String s) {
        int len = s.length();
        //越过的字符数量
        int skip = 0; 
        int left = len - 1; 
        StringBuilder sb = new StringBuilder();
        while (left >= 0) {
            //先消耗掉skip,消耗的同时,看看有没有#需要继续累加
            while (skip > 0 && left >= 0) {
                if (s.charAt(left) == '#') {
                    ++skip;
                } else {
                    --skip;
                }
                --left;
            }
            //防止left越界
            if (left < 0) {
                break;
            }
            //skip消耗完成,查看当前字符是否满足要求.
            if (s.charAt(left) != '#') {
                sb.append(s.charAt(left--));
            } else {
                ++skip;
                --left;
            }
        }
        return sb.toString();
    }
}

image-20230530231856751

当你能ac掉这道题后,你才有机会去跟面试官说你的优化思路,而且,你看这执行用时,其实也不输双指针了,主要就是优化一下空间。主要是思路还是一样,从后面开始,然后越过#和被#影响的字符,然后逐个比较,比较难的是最后的跳出循环和判断是否相等的条件。

  1. 如果是正常下标,不相等直接失败
  2. 如果是有一个是大于等于0,那么也是直接失败,因为最前面的处理#的代码,已经把#越过,并且处理完被影响的字符了。
class Solution {
    public boolean backspaceCompare(String s, String t) {
        int len1 = s.length(); int len2 = t.length();
        int left1 = len1 - 1; int left2 = len2 - 1;
        int skip1 = 0; int skip2 = 0;
        while (left1 >= 0 || left2 >= 0) {
            //遍历所有
            while (left1 >= 0) {
                //如果是#,需要跳过
                if (s.charAt(left1) == '#') {
                    ++skip1; 
                    --left1;
                //如果不是#,需要看看是否需要跳过
                } else if (skip1 > 0){
                    --skip1;
                    --left1; 
                } else {
                    break;
                }
            }
            while (left2 >= 0) {
                if (t.charAt(left2) == '#') {
                    ++skip2; 
                    --left2;
                } else if (skip2 > 0){
                    --skip2;
                    --left2; 
                } else {
                    break;
                }
            }

            //不相等,直接失败
            if (left1 >= 0 && left2 >= 0) {
                if (s.charAt(left1) != t.charAt(left2)) {
                    return false;
                }
                --left1;
                --left2;
            } 
            //存在一个>=0,表示另一个已经走完,而还有一个没有走完,直接失败
            else if (left1 >= 0 || left2 >= 0) {
                return false;
            } 
        }
        return true;
    }
}

image-20230531000801319

977. 有序数组的平方

思路:非递减顺序,所以左右两边的平方一定不比中间小,那么直接维护两边的指针即可,然后往中间走,直到碰头。

class Solution {
    public int[] sortedSquares(int[] nums) {
        int left = 0; int right = nums.length - 1;
        int[] res = new int[nums.length];
        int cnt = nums.length - 1;
        while (left <= right) {
            int a = nums[left] * nums[left];
            int b = nums[right] * nums[right];
            if (a < b) {
                res[cnt--] = b;
                --right;
            } else {
                res[cnt--] = a;
                ++left;
            }
        }
        return res;
    }
}

image-20230531001506305

反转字符串

344. 反转字符串

class Solution {
    public void reverseString(char[] s) {
        for (int i = 0, n = s.length / 2; i < n; ++i) {
            char temp = s[i];
            s[i] = s[s.length - i - 1];
            s[s.length - i - 1] = temp;
        }
    }
}

剑指 Offer 05. 替换空格

思路:既然这题出在了双指针中,那么就用双指针来做做。

class Solution {
    public String replaceSpace(String s) {

        char[] chars = s.toCharArray();
        int len = chars.length;
        int cnt = 0;
        //扩容
        for (char c: chars) {
            if (c == ' ') {
                cnt += 2;
            }
        }
        if (len == 0 || cnt == 0) {
            return s;
        }
        //从后面开始遍历,避免移动数字
        int left = len - 1;
        int right = cnt + len - 1;
        char[] res = Arrays.copyOf(chars, right + 1);
        while (left >= 0) {
            if (chars[left] == ' ') {
                res[right--] = '0';
                res[right--] = '2';
                res[right--] = '%';
            } else {
                res[right--] = chars[left];
            }
            --left;
        }
        return new String(res);
    }
}

image-20230531003827995

翻转字符串里的单词

151. 反转字符串中的单词

思路:这道题,之前做过,应该还有印象吧,主要就是一个局部反转和整体反转,只不过是顺序问题,你可以自己判断哪一种顺序会比较容易。这里我是采用一个先局部反转,再整体反转的思路,即先把每个单词进行反转,然后再把整个字符串进行反转即可。

只不过,你现在要怎么找到每一个单词的边界,很简单吧,就是一个空格,但是中间会出现很多个重复的空格,还有前后的空格,前后的空格好去,直接trim或者自己重新造一个字符串出来即可。中间多的空格,你就需要自己维护一个指针,去跳过了,但是你要注意,当你用了trim后,你用空格去判断单词,最后一个单词是没有空格的,所以我这里的做法是加了一个空格。

class Solution {
    public String reverseWords(String s) {
        s = s.trim();
        s += " ";
        StringBuilder sb = new StringBuilder();
        int start = 0;
        for (int end = 1; end < s.length(); ++end) {
            if (s.charAt(end) == ' ') {
                int r = end - 1;
                while (r >= start) {
                    sb.append(s.charAt(r--));
                }
                if (end != s.length() - 1) {
                    sb.append(" ");
                }
                start = end + 1;
                while (start < s.length() && s.charAt(start) == ' ') ++start;
                end = start;
            }
        }
        return sb.reverse().toString();
    }
}

代码和上次大差不差,有问题就可以回去字符串的4.4.1看一下。

翻转链表

206. 反转链表

对于一些之前写过的题目,这里就不再讲解析了,这里主要是作为一个复习。

头插法

//有头结点
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode dummpy = new ListNode(0, null);
        ListNode pre = head;
        ListNode last = head;
        while (pre != null) {
            last = pre.next;
            pre.next = dummpy.next;
            dummpy.next = pre;
            pre = last;
        }
        return dummpy.next;
    }
}
//无头结点
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        ListNode next;
        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

image-20230601205418848

class Solution {
    public ListNode reverseList(ListNode head) {
        Stack<ListNode> stack = new Stack<>();
        ListNode p = head;
        while (p != null) {
            stack.push(p);
            p = p.next;
        }
        ListNode dummpy = new ListNode();
        p = dummpy;
        while (!stack.isEmpty()) {
            p.next = stack.pop();
            p = p.next;
        }   
        p.next = null;
        return dummpy.next;
    }
}

image-20230601205930009

递归

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }
    private ListNode reverse(ListNode pre, ListNode cur) {
        if (cur == null) {
            return pre;
        } 
        ListNode head = reverse(cur, cur.next);
        cur.next = pre;
        return head;
    }
}

删除链表的倒数第N个节点

19. 删除链表的倒数第 N 个结点

递归

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummpy = new ListNode(0, head);
        remove(dummpy, head, n);
        return dummpy.next;
    }
    public int remove(ListNode pre, ListNode cur, int n) {
        if (cur == null) {
            return 0;
        }
        int sum = remove(cur, cur.next, n) + 1;
        if (sum == n) {
            pre.next = cur.next;
        }
        return sum;
    }
}

image-20230601211141697

双指针

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummpy = new ListNode(0, head);
        ListNode right = head;
        ListNode left = dummpy;
        for (int i = 0; i < n; ++i) {
            right = right.next;
        }
        while (right != null) {
            right = right.next;
            left = left.next;
        }
        left.next = left.next.next;
        return dummpy.next;
    }
}

image-20230601213152824

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummpy = new ListNode(0, head);
        Stack<ListNode> stack = new Stack<>();
        ListNode p = dummpy;
        while (p != null) {
            stack.push(p);
            p = p.next;
        }
        ListNode del = head;
        for (int i = 0; i < n; ++i) {
            del = stack.pop();
        }
        stack.pop().next = del.next;
        return dummpy.next;
    }
}

image-20230601213544754

链表相交

面试题 02.07. 链表相交

这题可以看看我上面的题解,写得挺不错的。2.6.1面试题 02.07. 链表相交

环形链表II

142. 环形链表 II

这题也是看我上面的题解,写得挺不错的。2.7.1环形链表II

三数之和

15. 三数之和

这题也是看我上面的题解,三数之和

四数之和

18. 四数之和

这题也是看我上面的题解,四数之和

栈和队列

用栈实现队列

232. 用栈实现队列

思路:就是两个羽毛球桶,互相倒过来倒过去的过程,如果是这样非常简单。

class MyQueue {

    Stack<Integer> left;
    Stack<Integer> right;
    int size;

    public MyQueue() {
        this.left = new Stack<>();
        this.right = new Stack<>();
        this.size = 0;
    }

    public void push(int x) {
        left.push(x);
        ++this.size;
    }

    public int pop() {
        while(!left.isEmpty()) {
            right.push(left.pop());
        }
        --size;
        int res = right.pop();
        while (!right.isEmpty()) {
            left.push(right.pop());
        }
        return res;
    }

    public int peek() {
        while(!left.isEmpty()) {
            right.push(left.pop());
        }
        int res = right.peek();
        while (!right.isEmpty()) {
            left.push(right.pop());
        }
        return res;
    }

    public boolean empty() {
        return this.size == 0;
    }
}

但是,你会发现,你pop的时候,总是要倒出来,然后再倒回去,可以不要再倒回去吗,或者说,不倒回去回去会影响最后的结果吗?

其实,不倒回去不会影响最后的结果,这里演示一下。

  1. push

left:1 2 3

right:

  1. pop

left:

right:3 2 1

当把球,第一次倒到另一个桶的时候,这时候如果不倒回去,如果这时候有push或者pop,这会有影响吗?没有影响,因为你如果对right继续pop的话,结果还是正确的2。而直到right为0的时候,我们再把left倒进去,结果依旧不会发生改变。

那么可以优化代码如下:

class MyQueue {

    Stack<Integer> left;
    Stack<Integer> right;
    int size;

    public MyQueue() {
        this.left = new Stack<>();
        this.right = new Stack<>();
        this.size = 0;
    }

    public void push(int x) {
        left.push(x);
        ++this.size;
    }

    public int pop() {
        if (right.isEmpty()) {
            while (!left.isEmpty()) {
                right.push(left.pop());
            }
        }
        --this.size;
        return right.pop();
    }

    public int peek() {
        if (right.isEmpty()) {
            while (!left.isEmpty()) {
                right.push(left.pop());
            }
        }
        return right.peek();
    }

    public boolean empty() {
        return this.size == 0;
    }
}

代码中的重复代码就没有去封装了。

用队列实现栈

225. 用队列实现栈

思路:这个队列就和栈的倒球不是很一样,因为你再怎么倒,你最后的队列的头还是没有改变,所以这里我们要换一个思路,在倒的时候,我们不要倒完,倒到最后一个的时候,就是我们需要pop出去的那个,我们保存一下值即可,然后再继续操作。

class MyStack {
    Queue<Integer> left;
    Queue<Integer> right;
    Integer size;

    public MyStack() {
        left = new LinkedList<>();
        right = new LinkedList<>();
        size = 0;
    }

    public void push(int x) {
        getBase().offer(x);
        ++size;
    }
    private Queue<Integer> getBase() {
        return size == left.size()
            ? left
            : right;
    }

    private Queue<Integer> getAnother() {
        return size == left.size()
            ? right
            : left;
    }

    public int pop() {
        int n = this.size - 1;
        int res;
        Queue<Integer> base = getBase();
        Queue<Integer> another = getAnother();
        while (n-- > 0) {
            another.offer(base.poll());
        }
        res = base.poll();
        --size;
        return res;        
    }

    public int top() {
        int res = pop();
        push(res);
        return res;
    }

    public boolean empty() {
        return size == 0;
    }   
}

image-20230602213300578

看了卡哥的思路,发现可以只用一个队列就完成这道题目,就是在插入的时候,通过轮转,已经就把所有元素的顺序已经倒置了,变成了一个倒置的队列(这里可以直接用双端队列即可)。那就按照这个思路,重新优化一下代码。

class MyStack {

    Queue<Integer> queue;

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

    public void push(int x) {
        queue.offer(x);
        int size = queue.size();
        //注意这里是少了一个没有出队.
        for (int i = 0; i < size - 1; ++i) {
            queue.offer(queue.poll());
        }
    }

    public int pop() {
        return queue.poll();
    }

    public int top() {
        return queue.peek();
    }

    public boolean empty() {
        return queue.isEmpty();
    }
}

这个是用双端队列写的,代码会更加简洁。

class MyStack {

    Deque<Integer> queue;

    public MyStack() {
        queue = new ArrayDeque<>();
    }

    public void push(int x) {
        queue.addLast(x);
    }

    public int pop() {
        return queue.pollLast();
    }

    public int top() {
        return queue.peekLast();
    }

    public boolean empty() {
        return queue.isEmpty();
    }
}

有效的括号

20. 有效的括号

思路:这道题,直接用栈去匹配即可,左括号进,右括号匹配,如果匹配错误,直接失败,成功就接着,最后返回的时候需要判断栈里面是否还有括号,如果还有也失败。

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (char c: s.toCharArray()) {
            if (isLeft(c)) {
                stack.push(c);
            } else {
                if (!stack.isEmpty()) {
                    char peek = stack.pop();
                    if (!match(peek, c)) {
                        return false;
                    } 
                } else {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }
    private boolean isLeft(char c) {
        return c == '(' || c == '{' || c == '[';
    }
    private boolean match(char left, char right) {
        if (left == '(') {
            return right == ')';
        } else if (left == '{') {
            return right == '}';
        } else {
            return right == ']';
        }

    }
}

image-20230602220101453

看了一下题解,发现还能这么做,直接遍历到左括号,就插入右括号,然后直到遍历到右括号的时候,栈顶必是相同的右括号,不过也需要去判断空的情况,这个思路也不难写出这样的代码。

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (char c: s.toCharArray()) {
            if (c == '(') {
                stack.push(')');
            } else if (c == '{') {
                stack.push('}');
            } else if (c == '[') {
                stack.push(']');
            } else if (stack.isEmpty() || stack.peek() != c) {
                return false;
            } else {
                stack.pop();
            }
        }
        return stack.isEmpty();
    }
}

image-20230602220516910

删除字符串中的所有相邻重复项

1047. 删除字符串中的所有相邻重复项

匹配问题都是栈的强项

思路:这道题,就是在遍历的过程,入栈,然后判断是否和前一个相等即可,然后去选择是否继续入栈,如果重复则出栈。

因为开始没有思考到最后输出倒序的结果,使用的是一端的栈,导致最后还需要反转,浪费了一点时间。

class Solution {
    public String removeDuplicates(String s) {
        Stack<Character> stack = new Stack<>();
        for (char c: s.toCharArray()) {
            if (stack.isEmpty() || stack.peek() != c) {
                stack.push(c);
            } else {
                stack.pop();
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()) {
            sb.append(stack.pop());
        }
        return sb.reverse().toString();
    }
}

image-20230602221239276

那么优化代码的思路就很简单了,直接使用双端队列,即可模拟栈,又可以成为队列顺序出字符串

class Solution {
    public String removeDuplicates(String s) {
        Deque<Character> deque = new ArrayDeque<>();
        for (char c: s.toCharArray()) {
            //入栈
            if (deque.isEmpty() || deque.peekFirst() != c) {
                deque.addFirst(c);
            } else {
                deque.pollFirst();
            }
        }
        StringBuilder sb = new StringBuilder();
        //出队
        while (!deque.isEmpty()) {
            sb.append(deque.pollLast());
        }
        return sb.toString();
    }
}

image-20230602221652419

然后小用了一下StringBuilder的api直接修改,尝试了一下。

class Solution {
    public String removeDuplicates(String s) {
        StringBuilder sb = new StringBuilder();
        int index = -1;
        for (char c: s.toCharArray()) {
            if (index >= 0 && c == sb.charAt(index)) {
                sb.deleteCharAt(index--);
            } else {
                sb.append(c);
                ++index;
            }
        }
        return sb.toString();
    }
}

image-20230602223844988

查看题解,发现还有一个双指针的做法,但是那个思路真的帅,在面试中很难想到这个思路,或者说写得和题解一样优雅,那个边界处理真的太优雅了,这里做一下尝试。

主要思路就是一个是遍历指针,还有一个是维护当前未有相邻重复的指针,只不过这个指针的遍历和赋值很讲究,如果不相等,那么就前移,并且覆盖,如果相等,那么维护的指针需要倒退。这个-1的边界处理雀食很是优雅。

class Solution {
    public String removeDuplicates(String s) {
        char[] ca = s.toCharArray();
        int index = -1;
        for (int i = 0; i < s.length(); ++i) {
            if (index == -1 || ca[index] != ca[i]) {
                ca[++index] = ca[i];
            } else {
                --index;
            }
        }
        return new String(ca, 0, index + 1);
    }
}

理解代码,最好还是得自己画一下图,去模拟跑一下才知道代码的优雅程度。

image-20230602231945293

逆波兰表达式求值

150. 逆波兰表达式求值

思路:如果知道如何通过逆波兰表达式求值的话,那么应该这题利用栈会非常容易实现,只需要push,直到为运算符的时候,pop前两个,然后进行运算即可。

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for (String s: tokens) {
            int res ;
            if ("+".equals(s)) {
                int b = stack.pop();
                int a = stack.pop();
                res = a + b;
            } else if ("-".equals(s)) {
                int b = stack.pop();
                int a = stack.pop();
                res = a - b;
            } else if ("*".equals(s)) {
                int b = stack.pop();
                int a = stack.pop();
                res = a * b;
            } else if ("/".equals(s)){
                int b = stack.pop();
                int a = stack.pop();
                res = a / b;
            } else {
                res = Integer.parseInt(s);
            }   
            stack.push(res);
        }
        return stack.pop();
    }
}

image-20230605201652406

但是,虽然这题没有要求,但是我这里还是拓展一下,如何把我们人类习惯见的中缀表达式,转换为计算机喜欢的后缀表达式,同样也是利用这个数据结构进行书写的,所以我感觉,在面试中很有几率会让你手搓一个出来,这里我简单地说一下求解的思路,这个思路还是有点复杂的。

遍历字符串

  1. 数字,直接输出
  2. 左括号,直接入栈
  3. 右括号,出栈并输出,直到遇到左括号(出栈但不输出)
  4. 运算符
  • 当栈为空、运算符的优先级大于栈顶运算符的优先级的时候(乘除大于加减)、栈顶运算符为左括号的时候,直接入栈
  • 否则,出栈并输出,while循环直到栈为空,或者左括号,或者运算符的优先级小于等于栈顶运算符的优先级,最后入栈。
  • 这个很好记忆,优先级大的先输出,一样等级或者小的话,就需要按顺序,把前面入栈的元素全部输出后才排到他。
  1. 最后把栈里面的运算符全部输出。

那么根据这个逻辑,可以写一份简单的代码,这只是一份很简单的代码,所以代码不是很优雅,而且只支持一个个位数的加减乘除,还有没有中缀表达式的格式校验,有兴趣的同学可以自己拓展一下,去加强代码的健壮性喔。

public class Main {
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        String str = s.nextLine();
        Stack<Character> stack = new Stack<>();
        for (char c : str.toCharArray()) {
            //左括号直接入栈
            if (c == '(') {
                stack.push(c);
            } 
            //右括号出栈直到左括号
            else if (c == ')') {
                while (stack.peek() != '(') {
                    System.out.print(stack.pop() + " ");
                }
                stack.pop();
            } 
            //运算符
            else if (c == '+' || c == '-' || c == '*' || c == '/') {
                //如果优先级高
                if (isPriorityHighOrLeft(c, stack)) {
                    stack.push(c);
                } else {
                    //优先级小于等于,那么就一直输出,直到高于栈顶等级或者为空或者为左括号
                    while (!stack.isEmpty() && !isPriorityHighOrLeft(c, stack)) {
                        System.out.print(stack.pop() + " ");
                    }
                    stack.push(c);
                }
            } else {
                System.out.print(c + " ");
            }
        }
        //最后需要推出栈里面的所有运算符
        while (!stack.isEmpty()) {
            System.out.print(stack.pop() + " ");
        }
    }

    private static boolean isPriorityHighOrLeft(char c, Stack<Character> stack) {
        if (stack.isEmpty()) {
            return true;
        }
        if (stack.peek() == '(') {
            return true;
        }
        //运算级高于栈顶
        if ((c == '*' || c == '/') && (stack.peek() == '+' || stack.peek() == '-')) {
            return true;
        }
        return false;
    }
}

滑动窗口最大值(保留,题解还有另外两种方法)

239. 滑动窗口最大值

当时看到这题的难度是困难的时候,确实有点想要退缩的感觉,但是一看题目,好像如果直接用暴力枚举来做,也是很简单,只不过八成是会超时的,不然为什么叫做困难呢?

那这题还是按照思路一步一步来,当然是用最简单的思路来做这道题,我们只需要找到每一个窗口的最大值即可,每走一次,找一次,然后存起来就行了,那么很快也可以写出代码。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length - k + 1];
        int cnt = 0;
        for (int i = 0; i <= nums.length - k; ++i) {
            res[cnt++] = findMax(nums, i, i + k - 1);
        }
        return res;
    }
    private int findMax(int[] nums, int left, int right) {
        int max = Integer.MIN_VALUE;
        for (int i = left; i <= right; ++i) {
            max = Math.max(nums[i], max);
        }
        return max;
    }
}

没问题,测试样例过了,提交!果然超时了,时间复杂度很容易知道,就是O(n* k)。

image-20230605213331700

所以,一定是每次都去找最大值的时候,浪费了很多时间,所以会不会是有什么方法,把前面的几个的最大值给存起来?然后超过窗口后,再把前面的给剔除了。一开始我想用的是优先队列,但是优先队列只是进去后会自动排序,但是当你超过窗口的时候,要剔除掉哪一个值就不知道了。然后还是去看了题解,发现用的是一个单调队列,这里来举个例子帮助大家理解这个单调队列。

就拿力扣的例子来讲。

1 3 -1 -3 5 3 6 7 —— 单调队列 ——初始状态遍历。

1 3 -1 -3 5 3 6 7 —— 1 ——无值,直接加入

1 3 -1 -3 5 3 6 7 —— 3 —— 你1比我3老滑动窗口向前遍历,生命周期1比3小),还比我3还力气小(只有小才剔除,如果一样大则不剔除)那么你永远没办法成为下一任的接班人(下一个滑动窗口的最大值)

1 3 -1 -3 5 3 6 7 —— 3 -1 —— 你-1虽然比我3力气小,但是当我3老了后,你可能就是下届力气最大的了,所以你先当我的候选人。

1 3 -1 -3 5 3 6 7 —— 3 -1 -3 ——1老了(生命周期到了),所以需要剔除出候选人的名单,但是你不在名单中,所以直接结束。接下来就是要选接班人和候选人了,你-3虽然比-1力气还更小,但是当3,-1老了后,-3就有可能还是力气最大的,所以先保留作为候选人。

1 3 -1 -3 5 3 6 7 —— 5 —— 3的生命周期到了,3就退休了,先pop掉,然后新来一个接班人5,发现队列中的-1,-3既比5老(生命周期短),还比5力气小,那么-1,-3永远也不可能再当接班人了。

1 3 -1 -3 5 3 6 7 —— 5 3 —— -1的生命周期结束,剔除。 3和-1,-3入队的规则同理。

1 3 -1 -3 5 3 6 7 —— 6 —— -3的生命周期结束,剔除,来了一个年少有为的6,直接把前面的接班人和候选人全部打败了。

1 3 -1 -3 5 3 6 7 —— 7 —— 5的生命周期结束,剔除,来了一个更加年少有为的7,最后就7作为接班人,当前遍历结束。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length - k + 1];
        int cnt = 0;
        Deque<Integer> deque = new ArrayDeque<>();
        for (int i = 0; i < nums.length; ++i) {
            push(deque, nums[i]);
            if (i - k + 1 >= 0) {
                res[cnt++] = getMax(deque);
                pop(deque, nums[i - k + 1]);
            }
        }
        return res;
    }
    private int pop(Deque<Integer> deque, int val) {
        if (!deque.isEmpty() && deque.peekFirst() == val) {
            return deque.removeFirst();
        }
        return -1;
    }

    private void push(Deque<Integer> deque, int val) {
        while (!deque.isEmpty() && deque.peekLast() < val) {
            deque.removeLast();
        }
        deque.addLast(val);
    }

    private int getMax(Deque<Integer> deque) {
        return deque.isEmpty() ? Integer.MIN_VALUE : deque.peekFirst();
    }
}

//最后的代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> deque = new ArrayDeque<>();
        int[] res = new int[nums.length - k + 1];
        int cnt = 0;
        for (int i = 0; i < nums.length; ++i) {
            while (!deque.isEmpty() && deque.peekLast() < nums[i]) {
                deque.pollLast();
            }
            deque.offerLast(nums[i]);
            if (i - k + 1 >= 0) {
                res[cnt++] = deque.peekFirst();
                if (!deque.isEmpty() && deque.peekFirst() == nums[i - k + 1]) {
                    deque.pollFirst();
                }
            }
        }
        return res;
    }
}

image-20230606204207606

前 K 个高频元素

347. 前 K 个高频元素

感觉一看就是要考一个新的数据结构,优先级队列,但是还没有接触过,但是,遇到难题,先别急,先来一波暴力求解先,尝试一下能不能ac掉再说。那么暴力的思路应该不用多讲啦,应该很简单,直接map计数,然后排序,取前几个数出来即可

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> hashMap = new HashMap<>(nums.length);
        //计数
        for (int i : nums) {
            hashMap.put(i, hashMap.getOrDefault(i, 0) + 1);
        }
        Set<Map.Entry<Integer, Integer>> entrySet = hashMap.entrySet();
        List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(entrySet);
        //倒序排序
        entries.sort((o1, o2)-> -(o1.getValue() - o2.getValue()));
        int[] res = new int[k];
        int cnt = 0;
        for (int i = 0; i < k; i++) {
            res[cnt++] = entries.get(i).getKey();
        }
        return res;
    }
}

image-20230606212500549

居然可以很顺利地A掉这个题目,那么现在就要去学习一下优先级队列的使用啦,之前还没有用过这个数据结构,只是听过。

优先级队列,里面的实现原理就是所谓的堆,里面可以分为小顶堆或者大顶堆。堆用来做此类前k个有序元素的题目,最适合不过了,因为,你的排序,你是排了一个非常大的数据量,而最后需要的结果只是前k个元素,而堆的话会自动帮我们去进行排序集合中的值,所以我们只要保证堆中元素为k个元素即可,就能一直保证前k频数的值了。快排:O(nlogn),堆:O(ologk)

这里先直接介绍一下java中的优先级队列,还有一些必要的api

        //默认排序规则就是小顶堆,从小到大,也就是e1 - e2;
        //和我们一般的排序的compare的接口一致的规则,从小到大是默认规则,实现接口也是第一个减去第二个.
        //大顶堆就-(e1- e2)
        PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>((e1, e2) -> e1.getValue() - e2.getValue());
        //然后因为堆的特性就是堆顶出,堆尾进,所以和队列一样的api,也是offer和poll.
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        //先使用hashmap记录频数
        Map<Integer, Integer> hashMap = new HashMap<>(nums.length);
        for (int i : nums) {
            hashMap.put(i, hashMap.getOrDefault(i, 0) + 1);
        }

        //默认排序规则就是从小到大,也就是e1 - e2;
        PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>((e1, e2) -> e1.getValue() - e2.getValue());

        //entrySet集合遍历
        for (Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
            //如果堆还没到达k个元素,直接添加
            if (pq.size() < k) {
                pq.offer(entry);
            }
            //如果来的数的频数太小,直接抛弃,只有当比小顶堆的堆顶的频数大的才有资格进入堆中
            else if (entry.getValue() > pq.peek().getValue()) {
                //移除掉小顶堆的堆顶,也就是把当前最小的频数的值剔除
                pq.poll();
                //加上当前的
                pq.offer(entry);
            }
        }
        int[] res = new int[k];
        int cnt = 0;
        for (int i = 0; i < k; i++) {
            res[cnt++] = pq.poll().getKey();
        }
        return res;
    }
}

image-20230606220031430

二叉树

前序遍历

144. 二叉树的前序遍历

递归

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<Integer>();
        preorderTraversal(list, root);
        return list;
    }
    private void preorderTraversal(List<Integer> list, TreeNode node) {
        if (node == null) {
            return ;
        }
        list.add(node.val);
        preorderTraversal(list, node.left);
        preorderTraversal(list, node.right);
    }
}

image-20230606223228897

迭代一

大家应该是一般都是递归遍历接触得比较多,递归应该也是可以比较快地写出来,但是换到了迭代法,可能就有点懵逼了,其实道理都是一样的迭代和递归,其实很多都是可以互相转换的,比如说这道题,递归,其实调用的是系统的栈,那么我们是不是可以自己用栈,来模拟系统的栈,然后来实现迭代法遍历二叉树呢?

对于前序遍历,我们使用栈,存根结点,pop出来,然后输出,然后push右孩子,再push左孩子即可,为什么先push右孩子,因为先进后出嘛,右先进,等会不就是左先输出了嘛。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root == null) return Collections.emptyList();
        stack.push(root);
        List<Integer> list = new ArrayList<>();
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            list.add(node.val);
            //注意顺序,栈是先进后出,所以先进去右孩子
            if (node.right != null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return list;
    }
}

image-20230607234208029

迭代二

但是相信大家对于为什么这么写还是有点疑惑,所以这里用另一版迭代代码来写,这个就是符合我们人类的思考方式,对于前序遍历,是不是就是要一直往左走,然后走到尾巴了,看看右边,然后继续往左走,不行就退回去,退回去这个操作,我们是利用栈来保留上一个位置的,而一直深入,就是利用while循环进行的。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        TreeNode p = root;
        List<Integer> list = new ArrayList<>();
        while (!stack.isEmpty() || p != null) {
            //当p不为空,那就一直往深处遍历
            while (p != null ) {
                //结果++
                list.add(p.val);
                //进栈,保留路径
                stack.push(p);
                //一直深入
                p = p.left;
            }
            //已经到头了,退出循环,看看有没有右孩子.
            p = stack.pop().right;
        }
        return list;
    }
}

image-20230608003453142

迭代三

统一迭代模板

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        List<Integer> list = new ArrayList<>();
        while (!stack.isEmpty()) {
            TreeNode p = stack.pop();
            if (p != null) {
                //右最慢输出,最先进去
                if (p.right != null) stack.push(p.right);
                if (p.left != null) stack.push(p.left);
                //最先输出的根,先进去,
                stack.push(p);
                //加空表明已经遍历过
                stack.push(null);
            } else {
                list.add(stack.pop().val);
            }
        }
        return list;
    }
}

image-20230608212635193

589. N 叉树的前序遍历

当你会了二叉树的前序遍历的递归后,对于n叉树也是手到擒来,二叉树是遍历左右,n叉树就遍历所有孩子结点就行了。

class Solution {
    public List<Integer> preorder(Node root) {
        List<Integer> list = new ArrayList<>();
        preorder(list, root);
        return list;
    }
    private void preorder(List<Integer> list, Node root) {
        if (root == null) return ;
        list.add(root.val);
        for (Node node: root.children) {
            preorder(list, node);
        }
    }
}

image-20230609211658571

后序遍历

145. 二叉树的后序遍历

递归

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<Integer>(); 
        postorderTraversal(list, root);
        return list;
    }
    private void postorderTraversal(List<Integer> list, TreeNode node) {
        if (node == null) return ;
        postorderTraversal(list, node.left);
        postorderTraversal(list, node.right);
        list.add(node.val);
    }
}

image-20230606223442753

迭代

而对于后序遍历的迭代法,就会比较地丑陋,其实他是直接利用前序遍历的迭代法,然后加上倒序的。

前序遍历的顺序是中左右,而后序遍历的顺序是左右中,其实主要是就是逆序,然后还记得之前的前序遍历为什么是先入右孩子的栈再入左孩子吗,就是为了控制输出的顺序,所以这里我们也可以小改动,便完成了后序遍历的迭代。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) return Collections.emptyList();
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        List<Integer> res = new ArrayList<>();
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.left != null) stack.push(node.left);
            if (node.right != null) stack.push(node.right);
        }
        Collections.reverse(res);
        return res;
    }
}

迭代二

这个就是一个统一的迭代法模板,改动就是已经遍历过的结点的两行代码的位置,谁最后输出,谁最先入栈。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        List<Integer> list = new ArrayList<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode p = stack.pop();
            if (p != null) {
                //根结点最后输出,所以最先进去
                stack.push(p);
                //入空结点,表明之前的是已经遍历过的,不需要继续遍历.直接输出即可
                stack.push(null);
                if (p.right != null) stack.push(p.right);
                if (p.left != null) stack.push(p.left);
            } else {
                list.add(stack.pop().val);
            }
        }
        return list;
    }
}

image-20230608212122239

590. N 叉树的后序遍历

同n叉树的前序遍历

class Solution {
    public List<Integer> postorder(Node root) {
        List<Integer> list = new ArrayList<>();
        postorder(list, root);
        return list;
    }
    private void postorder(List<Integer> list, Node root) {
        if (root == null) return ;

        for (Node node: root.children) {
            postorder(list, node);
        }
        list.add(root.val);
    }
}

image-20230609211934139

中序遍历

94. 二叉树的中序遍历

递归

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        inorderTraversal(list, root);
        return list;
    }
    private void inorderTraversal(List<Integer> list ,TreeNode node) {
        if (node == null) return;
        inorderTraversal(list, node.left);
        list.add(node.val);
        inorderTraversal(list, node.right);
    }
}

迭代一

先左再中后右,边深入边入栈,然后出栈输出最深一个,跳到右边,然后边深入边入栈。

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        List<Integer> list = new ArrayList<>();
        while (!stack.empty() || cur != null) {
            if (cur != null) {
                //边入栈边深入
                stack.push(cur);
                cur = cur.left;
            } else {
                //到最深了,出栈,输出,右转
                cur = stack.pop();
                list.add(cur.val);
                cur = cur.right;
            }
        }
        return list;
    }
}

迭代二

迭代模板

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        List<Integer> list = new ArrayList<>();
        while (!stack.isEmpty()) {
            TreeNode p = stack.pop();
            if (p != null) {
                if (p.right != null) stack.push(p.right);
                stack.push(p);
                stack.push(null);
                if (p.left != null) stack.push(p.left);
            } else {
                list.add(stack.pop().val);
            }
        }
        return list;
    }
}

二叉树的层序遍历

102. 二叉树的层序遍历

其实树的层序遍历和图的广度优先搜索都是利用队列这个结构去保存当前层的结点,还有一个size变量去保存当前层的结点个数,然后poll size次,就是当前层的元素了。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();

        List<List<Integer>> res = new ArrayList<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            //每层个数
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            //遍历当前层
            while (size-- > 0) {
                TreeNode cur = queue.poll();
                //添加左右
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
                list.add(cur.val);
            }
            res.add(list);
            //更新下一层的个数
        }
        return res;
    }   
}

image-20230608214853749

107. 二叉树的层序遍历 II

哈哈哈,这道题,怎么说呢,你可以观察一下上面102题的规律,从上到下,他是一层一层输出的,那么这道题,从底到上,也是一层一层输出的,那么不就是一个倒序的结果吗,所以我们只要把最后的结果倒序输出即可了。

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        List<List<Integer>> res = new ArrayList<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            while (size-- > 0) {
                TreeNode p = queue.poll();
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
                list.add(p.val);
            } 
            res.add(list);
        }
        Collections.reverse(res);
        return res;
    }
}

image-20230608222053703

199. 二叉树的右视图

都是同一个套路,这题只要遍历到每层最后一个添加到结果集中即可了。

class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        List<Integer> list = new ArrayList<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode p = queue.poll();
                if (size == 0) {
                    list.add(p.val);
                }
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
            }
        }
        return list;
    }
}

image-20230608222039054

637. 二叉树的层平均值

同理,只要遍历记录每一层的平局值即可

class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> list = new ArrayList<>();
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            int n = size;
            double sum = 0;
            while (size-- > 0) {
                TreeNode p = queue.poll();
                sum += p.val;
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
            }
            list.add(sum / n);
        }
        return list;
    }
}

image-20230608222440601

429. N 叉树的层序遍历

树和二叉树的层序遍历其实差不多的,只是入队时,孩子的遍历有点区别而已,其他基本都是一样的。

class Solution {
    public List<List<Integer>> levelOrder(Node root) {
        Queue<Node> queue = new ArrayDeque<>();
        List<List<Integer>> res = new ArrayList<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            while (size-- > 0) {
                Node p = queue.poll();
                list.add(p.val);
                for (Node n: p.children) {
                    queue.offer(n);
                }
            }
            res.add(list);
        }
        return res;
    }
}

image-20230608222906080

515. 在每个树行中找最大值

层序遍历的模板不变,只不过加了一个变量,记录每一层的最大数。

class Solution {
    public List<Integer> largestValues(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        List<Integer> list = new ArrayList<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            int max = Integer.MIN_VALUE;
            while (size-- > 0) {
                TreeNode p = queue.poll();
                max = Math.max(max, p.val);
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
            }
            list.add(max);
        }
        return list;
    }
}

image-20230608223359755

116. 填充每个节点的下一个右侧节点指针

依旧是一个层序遍历的模板,只不过把输出操作换成了一个拼接操作,那么只要把当前的元素,next指向队头即可(如果是每层最后一个,则指向null)

class Solution {
    public Node connect(Node root) {
        Queue<Node> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                Node p = queue.poll();
                p.next = size == 0 ? null: queue.peek();
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
            }
        }
        return root;
    }
}

image-20230608223953586

没想到把判断语句放在while里面,效率这么慢,优化一下,把if放在外面,减少小while里面的尾的判断,只不过代码会冗余一点。

class Solution {
    public Node connect(Node root) {
        Queue<Node> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            List<Integer> list = new ArrayList<>();
            Node pre = queue.poll();
            int size = queue.size();
            if (pre.left != null) queue.offer(pre.left);
            if (pre.right != null) queue.offer(pre.right);
            while (size-- > 0) {
                Node cur = queue.poll();
                pre.next = cur;
                pre = cur;
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
            }
            pre.next = null;
        }
        return root;
    }
}

image-20230628223430958

117. 填充每个节点的下一个右侧节点指针 II

和上面的题目一模一样,代码就不贴了

104. 二叉树的最大深度

这道题,也可以用层序遍历来做,只不过效率有点低罢了,但是也是能a掉

class Solution {
    public int maxDepth(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int deep = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode p = queue.poll();
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
            }
            ++deep;
        }
        return deep;
    }
}

image-20230608224810178

这道题,用递归来做是很完美的,二叉树的递归遍历其实就是一个深度优先搜索的过程,所以她能很快地到达底部,然后回来的时候就顺带一个深度回来了。

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        int deep1 = maxDepth(root.left) + 1;
        int deep2 = maxDepth(root.right) + 1;
        return Math.max(deep1, deep2);
    }
}

image-20230608224756514

111. 二叉树的最小深度

要读清楚题意,是叶子结点到根的距离才叫做深度。

class Solution {
    public int minDepth(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int deep = 0;
        int minDeep = Integer.MAX_VALUE;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode p = queue.poll();
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
                if (p.left == null && p.right == null) {
                    minDeep = Math.min(deep + 1, minDeep);
                }
            }
            ++deep;
        }
        return minDeep == Integer.MAX_VALUE ? 0 : minDeep;
    }
}

image-20230608231351418

然后看了一下题解,发现自己有点笨,都在遍历每一层了,所以只要这一层中,只要有一个是叶子,一定就是最小距离的叶子。

class Solution {
    public int minDepth(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int deep = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode p = queue.poll();
                if (p.left != null) queue.offer(p.left);
                if (p.right != null) queue.offer(p.right);
                if (p.left == null && p.right == null) {
                    return deep + 1;
                }
            }
            ++deep;
        }
        return deep;
    }
}

image-20230608233932347

然后这里尝试了一下递归,发现写得好丑陋,但是最后虽然还是过了。

class Solution {
    public int minDepth(TreeNode root) {
        int min = min(root);
        return min == Integer.MAX_VALUE ? 0: min;
    }

    private int min(TreeNode root) {
        if (root == null) return Integer.MAX_VALUE;
        if (root.left == null && root.right == null) return 1;
        int deep1 = min(root.left);
        deep1 = deep1 == Integer.MAX_VALUE ? deep1 : deep1 + 1;
        int deep2 = min(root.right);
        deep2 = deep2 == Integer.MAX_VALUE ? deep2: deep2 + 1;
        return Math.min(deep1, deep2);
    }
}

image-20230608233359439

然后再看了一下题解,发现如何规避掉所谓的只有一个空孩子,原来直接退出就行了。

class Solution {
    public int minDepth(TreeNode root) {
        //为空,直接0
        if (root == null) return 0;
        //叶子结点,开始返回深度
        if (root.left == null && root.right == null) return 1;
        //只有一个left为空,右不为空,所以从右遍历,最小的深度一定是当前深度+1
        if (root.left == null) {
            return minDepth(root.right) + 1;
        }
        //同上
        if (root.right == null) {
            return minDepth(root.left) + 1;
        }
        //走到这里,才是有叶子结点的路径,那么就要判断一下哪一个叶子路径最短了.
        int deep1 = minDepth(root.left);
        int deep2 = minDepth(root.right);
        return Math.min(deep1 + 1,deep2 + 1);
    }
}

image-20230610212529093

翻转二叉树

226. 翻转二叉树

递归

其实一些题目,都是基于遍历的递归代码,简单改一下就可以出解决题目的,就比如说这道题,翻转,一眼看上去其实没有什么思路对吧,但是其实翻转就是把左右孩子互换即可,然后你再看看前序遍历的递归模板。

if (root == null) return ;
method(root.left);
method(root.right);

那么当进入这个递归方法的时候,root是不是为根结点,那么在这里把两个左右孩子互换不就行了吗。

class Solution {
    public TreeNode invertTree(TreeNode root) {
        if (root == null) return root;
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
}

image-20230609205121767

迭代

重点来啦,前几天才学了通用的二叉树迭代,不会大家已经忘记了吧?那这里就需要再好好复习一下喽。那么这道题,用统一的迭代遍历模板也是可以做出来哒,还记得一个标记位吗,当遍历到那里的时候,就是我们的一个根结点,所以我们在那里进行交换左右孩子就行啦。

class Solution {
    public TreeNode invertTree(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode p = stack.pop();
            if (p != null) {
                TreeNode temp = p.left;
                p.left = p.right;
                p.right = temp;
                stack.push(p);
                stack.push(null);
                if (p.left != null) stack.push(p.left);
                if (p.right != null) stack.push(p.right);
            } else {
                stack.pop();
            }
        }
        return root;
    }
}

image-20230609210523507

对称二叉树

101. 对称二叉树

递归

这个就要打破之前一直写的递归遍历模板了,这道题的难点在于,如果你用一个结点去递归遍历的时候,你只能拿到对称线的一边,但是你要比较的时候,是要同时两边一起比较的,所以,有没有办法同时拿到一边一个,然后分别去遍历呢?是不是只要把递归方法改一下,改成两个不就行了。

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return isSymmetric(root, root);
    }
    private boolean isSymmetric(TreeNode left, TreeNode right) {
        //都为null
        if (left == null && right == null) return true;
        //只要有一个不为null
        if (left == null || right == null) return false;
        //不相等
        if (left.val != right.val) return false;
        //左右开弓
        return isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left);
    }
}

image-20230609213915752

其实代码很好理解,就拿这个图来说

image-20230609214100023

指针的遍历顺序如下,其中跳过了一些空结点的遍历。

  1. 左右指针指向根节点1
  2. 左指针指向左边的2,右指针指向右边的2
  3. 左指针指向左边的3,右指针指向右边的3
  4. 左指针指向左边的4,右指针指向右边的4

所以其实左右指针是一个镜像地在遍历这棵二叉树。

迭代

递归和迭代其实思路差不多,迭代也是利用两个指针,一个指向右边,一个指向左边,只不过这里需要利用一个队列把孩子或者同层的存一下而已。

class Solution {
    public boolean isSymmetric(TreeNode root) {
        //注意这里的实现需要使用LinkedList,ArrayDeque不允许存null
        Queue<TreeNode> deque = new LinkedList<>();
        if (root == null) return true;
        //存左右
        deque.offer(root.left); deque.offer(root.right); 
        while (!deque.isEmpty()) {
            //取左右指针
            TreeNode left = deque.poll(); TreeNode right = deque.poll();
            //同时为空,1
            if (left == null && right == null) continue;
            //一个为空,0
            if (left == null || right == null) return false;
            //值不相等,0
            if (left.val != right.val) return false;
            //注意这里的放入顺序,和递归是一样的,从两侧往中间去.
            deque.offer(left.left);
            deque.offer(right.right);
            deque.offer(left.right);
            deque.offer(right.left);
        }
        return true;
    }   
}

image-20230609221537852

100. 相同的树

递归

做了上面的那两道题,这道题岂不是又是手到擒来?一样都是两个指针,只不过唯一的区别就是一个是对称,一个是相等罢了,也就是比较的顺序不一样。

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null && q == null) return true;
        if (p == null || q == null) return false;
        if (p.val != q.val) return false;
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    } 
}

image-20230609221846207

迭代

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(p); queue.offer(q);
        while (!queue.isEmpty()) {
            TreeNode left = queue.poll(); TreeNode right = queue.poll();
            if (left == null && right == null) continue;
            if (left == null || right == null) return false;
            if (left.val != right.val) return false;
            queue.offer(left.left);
            queue.offer(right.left);
            queue.offer(left.right);
            queue.offer(right.right);
        }
        return true;
    }
}

image-20230609223448996

572. 另一棵树的子树

迭代

这道题,要最简单的做法,就是直接二叉树遍历+相同的树,两道题的答案合并一下就是这道题的答案了。

class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        Stack<TreeNode> stack = new Stack<>();
        if (root == null && subRoot == null) return true;
        stack.push(root);
        //前序遍历
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node.val == subRoot.val) {
                if (isSame(node, subRoot)) {
                    return true;
                }
            }
            if (node.right != null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return false;
    }

    //相同的树
    private boolean isSame(TreeNode p, TreeNode q) {
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(p); queue.offer(q);
        while (!queue.isEmpty()) {
            TreeNode left = queue.poll(); TreeNode right = queue.poll();
            if (left == null && right == null) continue;
            if (left == null || right == null) return false;
            if (left.val != right.val) return false;
            queue.offer(left.left);
            queue.offer(right.left);
            queue.offer(left.right);
            queue.offer(right.right);
        }
        return true;
    }
}

只不过一看就是暴力解法,效率应该比较差,这里走了一个前序遍历n(根树),然后还要再遍历一个m(子树),所以时间复杂度为O(n*m),但是应该是很好理解的代码。

image-20230609225606108

递归

一样的思路,遍历加相同树的判断。

class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (root == null) return false;
        boolean flag = isSubtree0(root, subRoot);
        return flag || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
    }

    private boolean isSubtree0(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 isSubtree0(left.left, right.left) && isSubtree0(left.right, right.right);
    }
}

image-20230629204335071

二叉树的最大深度

104. 二叉树的最大深度

见7.4.9

n叉树的最大深度

559. N 叉树的最大深度

迭代

层序遍历

class Solution {
    public int maxDepth(Node root) {
        Queue<Node> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int deep = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                Node cur = queue.poll();
                for (Node node: cur.children) {
                    queue.offer(node);
                }
            }
            ++deep;
        }
        return deep;
    }
}

递归

class Solution {
    public int maxDepth(Node root) {
        if (root == null) return 0;
        //注意这里,已经是有一层深度了.
        int max = 1;
        for (Node node: root.children) {
            max = Math.max(maxDepth(node) + 1, max);
        }
        return max;
    }
}

二叉树的最小深度

111. 二叉树的最小深度

看层序遍历那一章的最小深度。

完全二叉树的节点个数

222. 完全二叉树的节点个数

递归

也是利用一个前序遍历的递归模板,因为你前序遍历的结点,也其实就是个数了。

class Solution {
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        return countNodes(root.left) + 1 + countNodes(root.right);
    }
}

image-20230610213916889

迭代

有了一个递归模板,和一个迭代模板后,解这些题都是只要更换一个核心业务代码即可。

class Solution {
    public int countNodes(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        int size = 0;
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur != null) {
                if (cur.left != null) stack.push(cur.left);
                if (cur.right != null) stack.push(cur.right);
                ++size;
            }
        }
        return size;
    }
}

image-20230629210444438

但是这个前中后序遍历的模板的性能是有一点差的,因为有一个重复pop和push的动作,还有一个空标记位,所以会多了比较多的遍历,最好还是可以使用层序遍历的模板来解这道题。

class Solution {
    public int countNodes(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        int cnt = 0;
        if (root != null) queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode cur = queue.poll();
                ++cnt;
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
            }
        }
        return cnt;
    }
}

image-20230622215902243

好吧,性能是提升了,但是相比递归还是慢了很多呀。

平衡二叉树

*110. 平衡二叉树

递归

判断是否是平衡二叉树,主要是看高度差左右子树的高度差是否大于1,左右子树的遍历,我们任何一种遍历方式都行,但是找高度的话,我们是利用后序遍历去寻找的(而深度是前序遍历,可以通过数字去发现规律,高度是4321,所以是从下面往上,深度是1234,是从上面往下)。

class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) != -1;
    }
    private int getHeight(TreeNode root) {
        if (root == null) return 0;
        int leftH = getHeight(root.left);
        if (leftH == -1) return -1;
        int rightH = getHeight(root.right);
        if (rightH == -1) return -1;
        if (Math.abs(leftH - rightH) > 1) return -1;
        else return Math.max(leftH, rightH) + 1;
    }
}

迭代

迭代的思路也是差不多的,只不过可以通过任何一种遍历,查看左右子树的高度是否一样,而这里的高度,可以直接用最大深度去替换。

class Solution {
    public boolean isBalanced(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            int h1 = getHeight(node.left);
            int h2 = getHeight(node.right);
            if (Math.abs(h1 - h2) > 1) return false;
            if (node.left != null) stack.push(node.left);
            if (node.right != null) stack.push(node.right);
        }
        return true;
    }
    private int getHeight(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int depth = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            ++depth;
            while (size-- > 0) {
                TreeNode node = queue.poll();
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
        }
        return depth;
    }
}

image-20230629211733046

二叉树的所有路径

257. 二叉树的所有路径

这一题其实主要的问题是如何保存走过的路径,这里我采用的是字符串,因为字符串是不可变的,所以在根节点的时候,进去方法之后被修改的是不会影响到根节点的记录保存的,所以可以很好的保存原来的路径。路径就是要到叶子结点,也就是左右都为空。虽然想法很不错,但是代码写起来就可可碰碰的,然后最后的效率也不是很高。

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> list = new ArrayList<>();
        String str = null;
        return binaryTreePaths(list, root, str);
    }
    private List<String> binaryTreePaths(List<String> list, TreeNode root, String str) {
        if (root == null) return list;
        if (str == null) {
            str = root.val + "";
        } else {
            str += "->" + root.val;
        }
        if (root.left == null && root.right == null) {
            list.add(str);
            return list;
        }
        binaryTreePaths(list, root.left, str);
        binaryTreePaths(list, root.right, str);
        return list;
    }
}

image-20230623210005009

观察了一下题解,原来这个就是回溯的过程(如何找到原来的路径),我这里采用的一个字符串地址不可变的一个小技巧,巧妙地保留了原来的路径,但是因为会有频繁的字符串新建的过程,所以效率比较低,但是却很好地隐藏掉了一个回溯的过程,如果把里面的一些条件给改一改,会更加的优雅。

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> list = new ArrayList<>();
        String str = "";
        return binaryTreePaths(list, root, str);
    }
    private List<String> binaryTreePaths(List<String> list, TreeNode root, String str) {
        str += "->" + root.val;
        if (root.left == null && root.right == null) {
            list.add(str.substring(2));
            return list;
        }
        if (root.left != null) binaryTreePaths(list, root.left, str);
        if (root.right != null) binaryTreePaths(list, root.right, str);
        return list;
    }
}

勉强只能改到这样了,虽然效率还是不高,太多的字符串新建和拼接的操作了,所以不如暴露出一个栈,去进行拼接,只不过需要手动的回溯pop出最后的元素。

接下来就用一下集合,去手动维护一个走过的路径。

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> list = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        binaryTreePaths(list, root, path);
        return list;
    }
    private void binaryTreePaths(List<String> list, TreeNode root, List<Integer> path) {
        path.add(root.val);
        if (root.left == null && root.right == null) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0, n = path.size() - 1; i < n; ++i) {
                sb.append(path.get(i)).append("->");
            }
            sb.append(path.get(path.size()-1));
            list.add(sb.toString());
            return ;
        }
        if (root.left != null) {
            binaryTreePaths(list, root.left, path);
            path.remove(path.size() - 1);
        }
        if (root.right != null) {
            binaryTreePaths(list, root.right, path);
            path.remove(path.size() - 1);
        }
        return ;
    }
}

image-20230623220352555

而对于迭代法,那么就会比较抽象,和之前做的,虽然是同一个模板,但是如何去保存走过的路径是一个大问题,这里采用的是额外一个栈,去保存走过的路径。

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        //迭代法,主要思路是利用一个额外的栈,去存放每次走过的路径,需要冗余存储
        Stack<Object> stack = new Stack<>();
        List<String> list = new ArrayList<>();
        if (root != null) {
            stack.push(root);
            //存放第一个路径
            stack.push(root.val + "");
        }
        while (!stack.isEmpty()) {
            //路径
            String path = (String)stack.pop();
            //结点
            TreeNode node = (TreeNode)stack.pop();
            if (node != null) {
                //如果该结点是叶子结点,那么这个路径就是正确的,直接加入到结果集中
                if (node.left == null && node.right == null) {
                    list.add(path);
                    continue;
                }
                //如果不是叶子结点,那么需要把结点入栈,并保存该条路径,以便回溯
                if (node.left != null) {
                    stack.push(node.left);
                    stack.push(path + "->" + node.left.val);
                }
                if (node.right != null) {
                    stack.push(node.right);
                    stack.push(path + "->" + node.right.val);
                }
                //这里没有去push一个null,因为等到pop一个null出来后,也不需要任何的操作,所以直接省去.
            }
        }
        return list;
    }
}

image-20230623225258134

左叶子之和

404. 左叶子之和

递归

如果是遍历整个树,然后获取总和,你一定会吧,那这里只是多了一个条件,是左叶子,是否是叶子好判断,就是左右孩子都是空,而左呢?很简单,我们只要在方法的入参位置加一个判断条件即可。

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        return sumOfLeftLeaves(root, false);
    }
    private int sumOfLeftLeaves(TreeNode node, boolean isLeft) {
        if (node.left == null && node.right == null && isLeft) {
            return node.val;
        }
        int sum = 0;
        if (node.left != null) {
            sum += sumOfLeftLeaves(node.left, true);
        } 
        if (node.right != null) {
            sum += sumOfLeftLeaves(node.right, false);
        }
        return sum;
    }
}

image-20230625211548669

看了一下题解,他是从根结点开始,直接找到左叶子结点,然后相加,然后继续遍历的。代码如下:

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if (root == null) return 0;
        int sum = 0;
        if (root.left != null && root.left.left == null && root.left.right == null) {
            sum = root.left.val;
        }
        return sum + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
    }
}

image-20230625212151388

迭代

然后这里用迭代法——简化的前序遍历再做一次,思路也是很上面的第二种解法一样。

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        int sum = 0;
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node != null) {
                if (node.left != null && node.left.left == null && node.left.right == null) {
                    sum += node.left.val;
                }
                if (node.left != null) stack.push(node.left);
                if (node.right != null) stack.push(node.right);
            }
        }
        return sum;
    }
}

image-20230629214858473

找树左下角的值

513. 找树左下角的值

迭代

你可以从题目中的关键词,一眼就知道要用什么思路去做,最底层——层序遍历。然后你再想想层序遍历,最后一个遍历到的值,是哪个?是不是就是最底层的最右边的值呢?那如何要最左边的值呢?不就是从右往左层序遍历吗

class Solution {
    public int findBottomLeftValue(TreeNode root) {
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.offer(root);
        int value = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                TreeNode node = queue.poll();
                value = node.val;
                if (node.right != null) queue.offer(node.right);
                if (node.left != null) queue.offer(node.left);
            }
        }
        return value;
    }
}

image-20230629215122749

递归

这道题的递归,根本没有思路,只能用最原始的办法了,先求出最深的深度,然后再来一个遍历,直到找到第一个深度一样的。为了避免继续递归,找到第二个深度一样的,当找到第一个深度后,把最深的深度++,让他永远不会成立,唉真的是太丑陋了这个代码。

class Solution {
    int result = 0;
    int maxDeep = 0;
    public int findBottomLeftValue(TreeNode root) {
        maxDeep = findMaxDepth(root);
        find(root, 1);
        return result;
    }
    private void find(TreeNode root, int depth) {
        if (root == null) return ;
        if (depth == maxDeep) {
            result = root.val;
            ++maxDeep;
            return;
        }
        find(root.left, depth + 1);
        find(root.right, depth + 1);
        if (depth == maxDeep) result = root.val;
    }

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

但是?我是真的没有想到,这样的代码,居然能打败100

image-20230629222735361

但是,还是得看看题解,这样写真的太丑陋了。

看了题解,恍然大悟,为什么层序遍历的一直赋值的思想在这里没有想到呢?利用前序遍历,会一直找到最左边的结点,那么我每次拿到左结点,都给结果赋值,那么最后一个赋值不就是是答案了吗?而对于深度,每次递归满足叶子结点的时候,如果比最深的深度还深,那么就赋值最深深度,反之则略过。

class Solution {
    int result;
    int maxDepth = 0;
    public int findBottomLeftValue(TreeNode root) {
        result = root.val;
        find(root, 1);
        return result;
    }

    private void find(TreeNode root, int depth) {
        if (root == null) return ;
        if (root.left == null && root.right == null) {
            if (depth > maxDepth) {
                result = root.val;
                maxDepth = depth;
            }
        }
        find(root.left, depth + 1);
        find(root.right, depth + 1);
    }
}

image-20230629230317536

路径总和

112. 路径总和

递归

不知你是否还记得回溯,进去递归后变量改变,出来后,变量还原,所以这道题的关键就是把路径和进行增加和恢复。

class Solution {
    int res = 0;
    public boolean hasPathSum(TreeNode root, int targetSum) {
        //因为首次进入,树可能为空
        if (root == null) return false;
        // 路径++
        res += root.val;
        //如果是叶子结点,并且刚好值相等
        if (root.left == null && root.right == null && res == targetSum) return true;

        //存放结果值
        boolean flag = false;
        if (root.left != null) {
            flag = hasPathSum(root.left, targetSum);
            //主要是这个回溯的过程,需要把路径+的-回去
            res -= root.left.val;
        }

        if (root.right != null) {
            flag = flag || hasPathSum(root.right, targetSum);
            res -= root.right.val;
        }
        return flag;
    }
}

image-20230630213737235

因为这里我们使用了一个成员变量,那么我们看看是否可以把他给省去了。

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        //规避掉第一次树为空的情况
        if (root == null) return false;
        return hasPathSum0(root, targetSum);
    }

    private boolean hasPathSum0(TreeNode root, int targetSum) {
        if (root.val == targetSum && root.left == null && root.right == null) return true;
        boolean flag = false;
        //这种method(sum - val)是一种很经典的回溯写法,
        //因为当递归回来之后,sum不会改变,也就是达到了恢复的效果
        if (root.left != null)  flag = flag || hasPathSum0(root.left, targetSum - root.val);
        if (root.right != null)  flag = flag || hasPathSum0(root.right, targetSum - root.val);
        return flag;
    }
}

image-20230630214435533

看了一下题解,发现还可以更简单!

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;
        if (targetSum == root.val && root.left == null && root.right == null) return true;
        return hasPathSum(root.left, targetSum - root.val) 
            || hasPathSum(root.right, targetSum - root.val);
    }
}

迭代

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        //使用一个Object的栈,同时存放元素和走过的路径
        Stack<Object> stack = new Stack<>();
        if (root != null) {
            stack.push(root);
            stack.push(root.val);
        }
        while (!stack.isEmpty()) {
            int sum = (int) stack.pop();
            TreeNode node = (TreeNode) stack.pop();
            if (sum == targetSum && node.left == null && node.right == null) return true;
            if (node.left != null) {
                stack.push(node.left);
                stack.push(node.left.val + sum);
            }

            if (node.right != null) {
                stack.push(node.right);
                stack.push(node.right.val + sum);
            }
        }
        return false;
    }
}

image-20230630215223988

从中序与后序遍历序列构造二叉树

106. 从中序与后序遍历序列构造二叉树

如果你不知道这题的做题的步骤,直接想这道题还是有点难的。这题主要是抓住中序遍历和后序遍历的关键顺序

中序遍历:左

后序遍历:左右

然后我们通过这两个顺序,去定位每一个根结点。左右,不知道中在哪里,但是后序遍历的左右可以知道,最后一个就是中间结点!然后我们再根据该值,去分割中序遍历的数组,便可以得到该中间结点的左右两部分子树。

中序遍历:9 3 15 20 7 左右(被3分割后,可以知道,左边一个结点是左子树的,右边三个是右子树的)

后序遍历:9 15 7 20 3 左右(通过最后一个分割,然后找中序遍历的分割点,然后再通过分割后的左右个数,再分割后续遍历的左右部分。

中序遍历:9 15 20 7

后序遍历:9 15 7 20

中序遍历:15 7

后序遍历:15 7

根据这个思路,我们便可以写出如下的代码,这个是我的第一版代码,思路是完全按照上面来的,没有优化。

class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        TreeNode node = new TreeNode();
        //通过左右边界值去限制左右数组
        buildTree(inorder, 0, inorder.length - 1, 
        postorder, 0, postorder.length - 1, node);
        return node;
    }

    private void buildTree(int[] inorder, int l1, int r1,
     int[] postorder, int l2, int r2, TreeNode node) {
         //如果这个索引不合法了,直接退出,表示已经没有值了
         if (l1 > r1) return ;
         //后序遍历的最后一个值就是中间的结点
         node.val = postorder[r2];
         //找到分割点
         int mid = 0;
         for (int i = l1; i <= r1; ++i) {
             //中序遍历中与后序遍历最后一个相等的值
             if (inorder[i] == postorder[r2]) {
                mid = i;
                break;
             }
         }
        //索引是否合法?
        //mid为中序遍历分割点
        //中序遍历: l1 ~ mid - 1  mid  mid + 1 ~ r1
        //后序遍历的分割点是以中序遍历分割后的左右大小进行分割的
        //后序遍历: 左部分: l2 ~ l2 + (mid - l1) - 1    
        //         右部分: (r2 - 1) - (r1 - mid) + 1 ~ r2 - 1 
         if (mid - 1 >= l1) {
             //合法的话,表示还有值,new一个结点
            node.left = new TreeNode();
            buildTree(inorder, l1, mid - 1, 
            postorder, l2, mid - l1 + l2 - 1, node.left); 
         } 

         if (mid + 1 <= r1) {
            node.right = new TreeNode();
            buildTree(inorder, mid + 1, r1, 
            postorder, r2 - (r1 - mid), r2 - 1, node.right);
         }
     }
}

image-20230630231309001

看了一下题解的优化过程,因为题目规定了,数字都是唯一的,所以找分割点的时候,我们可以直接使用HashMap中一次性索引位置,减少每次都去遍历一次查找。代码中省去了注释,主要就是把查找分割点改为通过hashmap查找。

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        TreeNode node = new TreeNode();
        for (int i = 0; i < inorder.length; ++i) {
            map.put(inorder[i], i);
        }
        buildTree(inorder, 0, inorder.length - 1, 
        postorder, 0, postorder.length - 1, node);
        return node;
    }

    private void buildTree(int[] inorder, int l1, int r1,
     int[] postorder, int l2, int r2, TreeNode node) {
         node.val = postorder[r2];
         int mid = map.get(postorder[r2]);
         if (mid - 1 >= l1) {
            node.left = new TreeNode();
            buildTree(inorder, l1, mid - 1, 
            postorder, l2, mid - l1 + l2 - 1, node.left); 
         } 

         if (mid + 1 <= r1) {
            node.right = new TreeNode();
            buildTree(inorder, mid + 1, r1, 
            postorder, r2 - (r1 - mid), r2 - 1, node.right);
         }
     }
}

image-20230630232114263

105. 从前序与中序遍历序列构造二叉树

继续来一道巩固一下记忆,当你知道了做这题的步骤之后,其实是很简单的,只不过有很多细节需要注意,比如说,计算左右数组的边界,你的左右边界是闭的还是开的?有没有判断索引位置是否合法等等等等,都是你需要注意的。这里我重写了一版比较好理解的,把变量都声明出来了。

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        for (int i = 0; i < inorder.length; ++i) {
            map.put(inorder[i], i);
        }
        TreeNode node = new TreeNode();
        //注意左右边界都是闭的
        buildTree(preorder, 0, preorder.length - 1,
                inorder, 0, inorder.length - 1,
                node);
        return node;
    }
    private void buildTree(int[] preorder, int l1, int r1,
                           int[] inorder, int l2, int r2, 
                           TreeNode node) 
    {
        node.val = preorder[l1];
        int mid = map.get(preorder[l1]);
        //左子树的结点数量
        int leftNum = mid - l2;
        //右子树的结点数量
        int rightNum = r2 - mid;
        if (leftNum > 0) {
            node.left = new TreeNode();
            buildTree(preorder, l1 + 1, l1 + leftNum,  
                    inorder, l2, mid - 1,
                    node.left);
        }

        if (rightNum > 0) {
            node.right = new TreeNode();
            buildTree(preorder, r1 - rightNum + 1, r1,
                    inorder, mid + 1, r2,
                    node.right);
        }
    }
}

image-20230630233759398

最大二叉树

654. 最大二叉树

这道题,当你会写了上面的两道题后,你会发现,这个题目非常简单,构建二叉树的过程是一模一样的,而且还不用涉及到两个遍历的顺序。

class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        TreeNode node = new TreeNode();
        construct(nums, 0, nums.length - 1, node);
        return node;
    }
    private void construct(int[] nums, int left, int right, TreeNode node) {
        int max = Integer.MIN_VALUE;
        int maxIndex = left;
        for (int i = left; i <= right; ++i) {
            if (max < nums[i]) {
                maxIndex = i;
                max = nums[i];
            }
        }
        node.val = max;
        if (maxIndex > left) {
            node.left = new TreeNode();
            construct(nums, left, maxIndex - 1, node.left);
        }
        if (maxIndex < right) {
            node.right = new TreeNode();
            construct(nums, maxIndex + 1, right, node.right);
        }
    }
}

image-20230701210143021

合并二叉树

*617. 合并二叉树

递归

太痛苦了,这道题,不知道要怎么写去拿到走过的结点,因为这样才能通过原来的结点拼接下一个结点。最后没有办法,就通过方法参数的传递,传入上一个走过的结点,代码是非常地抽象非常地丑,缝缝补补写出来的代码,但是,结果!我是真的没有想到,居然就一次性过了,而且100%。

class Solution {
    public TreeNode mergeTrees(TreeNode left, TreeNode right) {
        //利用头指针的思想,去获取pre结点
        //最后的结果全部填在left树上,最后直接返回left树即可.
        TreeNode l = new TreeNode();
        l.left = left;
        TreeNode r = new TreeNode();
        r.left = right;
        mergeTrees(left, l, right, r);
        return l.left;
    }
    private void mergeTrees(TreeNode left, TreeNode lpre, TreeNode right, TreeNode rpre) {
        //如果两个都有值,就直接相加
        if (left != null && right != null) left.val += right.val;
        //因为是left树为主树,所以右树为空,直接结束
        else if (left != null && right == null) return;
        //左树为空,右树有的话,就需要新建结点,然后通过方法传进来的上一个结点,去拼接新的一个结点
        else if (left == null && right != null) {
            //上一个结点,是左子为空,还是右子为空,需要判断
            if (lpre.left == null && rpre.left != null) {
                lpre.left = new TreeNode(right.val);
                //注意最后需要赋值.
                left = lpre.left;
            } else {
                lpre.right = new TreeNode(right.val);
                left = lpre.right;
            } 
        } 
        //如果左右都为空,直接结束即可
        else if (left == null && right == null) return ;
        mergeTrees(left.left, left, right.left, right);
        mergeTrees(left.right, left, right.right, right);
    }
}

image-20230701213954362

看了题解,沉默了,就几行代码而已,有点笨了,从头到尾完全不用new出结点,就可以拿下这道题。

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        //主要就是看之前的结点要拼接哪棵树,而且很好的规避掉了两个都为null的判断
        if (root1 == null) return root2;
        if (root2 == null) return root1;
        //都有值就直接相加
        root1.val += root2.val;
        //这一步是精髓,当前结点的下面指向什么.
        root1.left = mergeTrees(root1.left, root2.left);
        root1.right = mergeTrees(root1.right, root2.right);
        return root1;
    }
}

哈哈!效率还没有我的高。

image-20230701214803534

迭代

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        //特判,避免开始的逻辑存在空的情况
        if (root1 == null) return root2;
        if (root2 == null) return root1;
        Stack<TreeNode> stack = new Stack<>();
        //注意顺序,先出为左,后为右
        stack.push(root2); stack.push(root1);
        while (!stack.isEmpty()) {
            TreeNode left = stack.pop(); TreeNode right = stack.pop(); 
            //特判过了,后面的逻辑也只有不为空才能进,所以直接相加
            left.val += right.val;
            //都不为空,直接入栈
            if (left.left != null && right.left != null) {
                stack.push(right.left); stack.push(left.left);
            } else {
                //如果是左为空的话,需要指向右树
                if (left.left == null) {
                    left.left = right.left;
                }
            }

            if (left.right != null && right.right != null) {
                stack.push(right.right); stack.push(left.right);
            } else {
                if (left.right == null) {
                    left.right = right.right;
                }
            }
        }
        return root1;
    }
}

image-20230701221511786

当我写了这个迭代后,恍然大悟,第一版代码的问题是不知道如何找到上次走过的路,发现迭代这样的写法,完全不需要,直接在当前结点,去判断子结点的情况就可以了,那么就可以通过这个迭代,去改造我的递归了。

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null) return root2;
        if (root2 == null) return root1;
        root1.val += root2.val;
        if (root1.left != null && root2.left != null) 
            mergeTrees(root1.left, root2.left); 
        else {
            if (root1.left == null) 
                root1.left = root2.left;
        }

        if (root1.right != null && root2.right != null) 
            mergeTrees(root1.right, root2.right); 
        else {
            if (root1.right == null) 
                root1.right = root2.right;
        }
        return root1;
    }
}

image-20230701222154827

二叉搜索树中的搜索

700. 二叉搜索树中的搜索

二叉搜索树,左子比父小,右子比父大。

递归

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null) return null;
        if (root.val == val) return root;
        if (root.val < val) {
            return searchBST(root.right, val);
        } else {
            return searchBST(root.left, val);
        }
    }
}

迭代

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node.val == val) return node;
            if (node.val < val && node.right != null) stack.push(node.right);
            else if (node.val > val && node.left != null) stack.push(node.left); 
        }
        return null;
    }
}

image-20230701223424433

然后看了一下题解,发现迭代还可以更容易!利用栈,是为了一个回溯的过程,而二叉搜索树,已经就可以通过值,确定要走的路径了(不会回头,你可以看到走的路径其实就是一个链表),所以直接一直往下走就好了。

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        while (root != null) {
            if (root.val == val) return root;
            else if (root.val < val) root = root.right;
            else root = root.left;
        }
        return null;
    }
}

最好是利用另外一个新结点去遍历,但是这里为了效率,直接就改变根结点了。

image-20230701223814107

验证二叉搜索树

98. 验证二叉搜索树

递归

这道题的解决关键是中序遍历的结果,二叉搜索树中序遍历的结果是有序的一个数组,我们便可以利用此进行判断。我们不需要等到全部遍历完再去遍历看看是否是有序,只要再每次添加的时候,保证比前面的大就好了。

class Solution {
    public boolean isValidBST(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        return isValidBST(root, list);
    }
    private boolean isValidBST(TreeNode root, List<Integer> list) {
        if (root == null) return true;
        boolean f1 = isValidBST(root.left, list);
        if (!list.isEmpty() && list.get(list.size() - 1) >= root.val) return false;
        list.add(root.val);
        f1 = f1 && isValidBST(root.right, list);
        return f1;
    }
}

image-20230701225803526

还可以继续优化,因为你比较的只是前一个数字,所以其实没有必要使用一个List去存放,只要一个变量就够了。

class Solution {
    //这里用long,因为有测试样例是int的最小值.
    long flag = Long.MIN_VALUE;
    public boolean isValidBST(TreeNode root) {
        if (root == null) return true;
        boolean f1 = isValidBST(root.left);
        if (flag >= root.val) return false;
        flag = root.val;
        f1 = f1 && isValidBST(root.right);
        return f1;
    }
}

迭代

class Solution {
    public boolean isValidBST(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        long flag = Long.MIN_VALUE;
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode p = stack.pop();
            if (p != null) {
                if (p.right != null) stack.push(p.right);
                //根结点最后输出,所以最先进去
                stack.push(p);
                //入空结点,表明之前的是已经遍历过的,不需要继续遍历.直接输出即可
                stack.push(null);
                if (p.left != null) stack.push(p.left);
            } else {
                p = stack.pop();
                if (flag >= p.val) return false;
                flag = p.val;
            }
        }
        return true;
    }
}

image-20230701231526884

二叉搜索树的最小绝对差

530. 二叉搜索树的最小绝对差

依旧是利用二叉搜索树中序遍历有序的规律,最小差值一定是存在于排序后的某两个数中间。主要思路就是利用一个值,去保存他上一次走过的路,就就是中序遍历的上一节点,这里直接用值保留即可。

class Solution {
    int diff = Integer.MAX_VALUE;
    int pre = Integer.MAX_VALUE;
    public int getMinimumDifference(TreeNode root) {
        if (root == null) return 0;
        getMinimumDifference(root.left);
        diff = Math.min(Math.abs(pre - root.val), diff);
        pre = root.val;
        getMinimumDifference(root.right);
        return diff;   
    }
}

image-20230702201735891

二叉搜索树中的众数

*二叉搜索树中的众数

这道题和上面的类似,都是需要一个值去保留上一个走过的节点,然后去判断是否相同,然后去计数,直到相等就添加,如果大于之前记录最大的话,就需要清空之前保留的,然后添加当前的。

class Solution {
    int maxCount = 0;
    int count = 0;
    TreeNode pre = null;
    public int[] findMode(TreeNode root) {
        Set<Integer> set = new HashSet<>();
        findMode(root, set);
        int[] res = new int[set.size()];
        int index = 0;
        for(int i: set) {
            res[index++] = i;
        }
        return res;
    }
    private void findMode(TreeNode node, Set<Integer> set) {
        if (node == null) return ;
        findMode(node.left, set);
        if (pre == null || pre.val != node.val) {
            count = 1;
        } else {
            ++count;
        }
        if (maxCount == count) {
            set.add(node.val);
        } else if (maxCount < count) {
            set.clear();
            set.add(node.val);
            maxCount = count;
        }
        pre = node;
        findMode(node.right, set);
    }
}

二叉树的最近公共祖先

*236. 二叉树的最近公共祖先

这个还是有点难理解的,这道题其实就是一道经典的回溯的题目,很好地利用了后序遍历,等到遍历完,再往上走的特点。当你回溯的时候,你这个结点是p或者q,你就直接返回当前结点,跟上面的结点说,我已经找到了p或者q啦。那么你现在就只需要继续往上走,直到找到一个结点,你的左子树找到了p或者q,同时右子树找到了p或者q,那么当前结点就是公共祖先。其他时候,只需要返回找到了节点的那个结点即可。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;
        //后序遍历,等到最下面回溯的时候,才开始处理左右子树.
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //表示当前结点就是p或者q,通知父结点你就是路径.
        if (root == p || root == q) return root;
        //当前结点的左右子树都找到了节点,这表明了当前就是公共祖先
        if (left != null && right != null) return root;
        //找到p或者q的路径的中的一个结点.继续告知上面我找到了p或者q
        if (left != null || right != null) 
            return left == null 
                ? right
                : left;
        return null;
    }
}

image-20230702214158425

二叉搜索树的最近公共祖先

235. 二叉搜索树的最近公共祖先

刚做完上面的题,当然先用一样的代码热热手先,虽然效率一定比较低。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if (root == p || root == q || (left != null && right != null)) return root;
        if (left != null || right != null) return left == null ? right : left;
        return null;
    }
}

image-20230702215736393

但是虽然有了二叉搜索树的特性,但是依旧不知道怎么优化,看了题解后,惊叹于如此美妙的想法,当比两个结点的值都小,那么就去找右边的,如果比两个节点都的值都大,那么就去找左边的,最后点睛之笔为,如果值是在两个中间,或者有一个相等的话,那么他就是公共祖先,而且是最近公共祖先。为什么就是最近公共祖先呢?以这个图为例,我们要找0和7的最近公共祖先,那么第一结点6就是符合要求了,那么就表示,p和q分别分布在6的左右子树中,这时候,如果我6往下遍历,那么救必定会错过一边的子树,比如说往左走,那么一开始的右子树就丢了。

image-20230702223212288

递归

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;
        TreeNode left = null, right = null;
        if (root.val > Math.max(p.val, q.val)) {
            left = lowestCommonAncestor(root.left, p, q);
        } else if (root.val < Math.min(p.val, q.val)) {
            right = lowestCommonAncestor(root.right, p, q);
        } else {
            return root;
        }
        return left == null ? right : left;
    }
}

image-20230702223403461

迭代

一样的思路

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur.val > Math.max(p.val, q.val)) stack.push(cur.left);
            else if (cur.val < Math.min(p.val, q.val)) stack.push(cur.right);
            else return cur;
        }
        return null;
    }
}

image-20230702224338789

二叉搜索树中的插入操作

701. 二叉搜索树中的插入操作

这道题可以不用去旋转树,所以主要就是找到哪一个结点就是要你插入的结点,我们需要判断,当前结点的值与要插入的值的关系,同时还要判断当前结点的将被插入的结点为空,最后利用BST的性质进行遍历即可。

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null) return new TreeNode(val);
        return ainsertIntoBST(root, val);
    }

    private TreeNode ainsertIntoBST (TreeNode root, int val) {
        if (root == null) return root;
        if (root.val < val) {
            if (root.right == null) root.right = new TreeNode(val);
            else insertIntoBST(root.right, val);
        }
        else {
            if (root.left == null) root.left = new TreeNode(val);
            else insertIntoBST(root.left, val);
        } 
        return root;
    }
}

image-20230703201240220

迭代也是很简单,和递归一模一样的思路。

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        Stack<TreeNode> stack = new Stack<>();
        if (root == null) return new TreeNode(val);
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur.val < val && cur.right == null) {
                cur.right = new TreeNode(val); break;
            }
            if (cur.val > val && cur.left == null) {
                cur.left = new TreeNode(val); break;
            }
            if (cur.val < val && cur.right != null) stack.push(cur.right); 
            if (cur.val > val && cur.left != null) stack.push(cur.left);
        }
        return root;
    }
}

image-20230703201811695

删除二叉搜索树中的节点

*450. 删除二叉搜索树中的节点

终于来到这题了,需要去调整二叉树的结构的题目,这道题,其实也不算很难,主要是把所有的情况都罗列一遍就好了。

  1. 找不到删除的结点(退出)
  2. 找到的结点的左右结点都为空(直接删除)
  3. 找到的结点的左子树为空,右子树不为空(父结点接右子树)
  4. 找到的结点的右子树为空,左子树不为空(父结点接左子树)
  5. 找到的结点的左右子树都不为空(这个下面讨论)

如果左右子树都不为空的话,那么最后需要从左右找一个继任者去继承当前的位置,这里选择左结点进行讨论,既然选了左子树,那么右子树应该怎么去拼接在左子树中呢?其实很简单,右子树一定比所有左子树的结点都大,那么直接拼接在左子树中最大结点的右结点即可!

这里还设置了pre结点,方便大家理解

class Solution {
    TreeNode pre = null;
    public TreeNode deleteNode(TreeNode root, int key) {
        TreeNode head = new TreeNode();
        head.left = root;
        pre = head;
        deleteNode0(root, key);
        return head.left;
    }
    private void deleteNode0(TreeNode root, int key) {
        if (root == null) return ;
        if (root.val == key) {
            //叶子结点
            if (root.left == null && root.right == null) {
                if (pre.left == root) pre.left = null;
                else pre.right = null;
            } 
            //左空,右不空
            else if (root.left == null && root.right != null) {
                if (pre.left == root) pre.left = root.right;
                else pre.right = root.right;
            }
            //右空,左不空
            else if (root.left != null && root.right == null) {
                if (pre.left == root) pre.left = root.left;
                else pre.right = root.left;
            }
            //左右都不为空
            else {
                getMax(root.left);
                max.right = root.right;
                if (pre.left == root) pre.left = root.left;
                else pre.right = root.left;
            }
            return ;
        }
        pre = root;
        if (root.val > key) deleteNode0(root.left, key);
        else deleteNode0(root.right, key);        
    }   
    //获取左子树中最大的结点
    TreeNode max = null;
    private void getMax(TreeNode root) {
        if (root == null) return ;
        getMax(root.left);
        max = root;
        getMax(root.right);
    }
}

image-20230703215511939

看了题解后,发现我上面的代码其实是迭代法,所以我们上面的代码可以很快地改为迭代,但是真正的递归和我的代码还是有点区别的,上面那样写,需要去判断pre结点的左还是右去拼接结点,而我们可以写成,直接返回需要拼接的结点,由上一层调用去决定谁来拼接。

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        root = adelete(root, key);
        return root;
    }

    private TreeNode adelete(TreeNode root, int key) {
        if (root == null) return root;
        if (root.val == key) {
            //这里挺优雅的,优化我上面那个代码的判断,如果两个都为null,其实赋值另一个也是一样的
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            //找出左子树中最大的一个结点
            TreeNode temp = root.left;
            while (temp.right != null) {
                temp = temp.right;
            }
            //最大结点的右拼接当前结点的右子树
            temp.right = root.right;
            //把当前结点删了.
            root = root.left;
        } else if (root.val > key) {
            //这里就不需要再用一个pre结点去保留上一个了,而是通过当前结点去决定我拼接哪里
            root.left = adelete(root.left, key);
        } else {
            root.right = adelete(root.right, key);
        }
        //直接返回root即可
        return root;
    }
}

image-20230703222055727

将有序数组转换为二叉搜索树

108. 将有序数组转换为二叉搜索树

递归

这道题很简单,还记得如何通过中序遍历和后序遍历的结果构造一颗二叉树吗,其实这里差不多的模板,也是核心找到一个分割点,而为了构成一颗平衡的二叉树,那么我们的分割点直接找到中间结点即可,然后不断的往下缩小,二分法,最后就可以构造出一颗平衡二叉搜索树。

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return sortedArrayToBST(nums, 0, nums.length - 1, new TreeNode());
    }

    public TreeNode sortedArrayToBST(int[] nums, int left, int right, TreeNode node) {
        //需要在这里先判断传进来的索引是否合法,不合法表示数组已经没有值了
        if (left > right) return null;
        //中间
        int mid = left + ((right - left) >> 1);
        //node已经被传递进来一个对象了
        node.val = nums[mid];
        //左子树,mid - 1
        node.left = sortedArrayToBST(nums, left, mid - 1, new TreeNode());
        //右子树,mid + 1
        node.right = sortedArrayToBST(nums, mid + 1, right, new TreeNode());
        return node;
    }
}

image-20230703225751478

迭代

这个其实还是有点难的,但是之前有一题的思路和这个很类似,就是一个拼接路径的题目,用一个容器,存放结点的同时,又存放了走过的路径,这就可以让我们很好的回溯。这道题也是一样的。用一个容器,存放结点的同时,又存放了需要进行切分的left值和right值。

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        Queue<Object> queue = new ArrayDeque();
        TreeNode root = new TreeNode();
        queue.offer(root);
        queue.offer(0);
        queue.offer(nums.length - 1);
        while (!queue.isEmpty()) {
            TreeNode cur = (TreeNode)queue.poll();
            int left = (int)queue.poll();
            int right = (int)queue.poll();
            int mid = left + ((right - left) >> 1);
            cur.val = nums[mid];
            if (left <= mid - 1) {
                cur.left = new TreeNode();
                queue.offer(cur.left);
                queue.offer(left);
                queue.offer(mid - 1);
            }

            if (right >= mid + 1) {
                cur.right = new TreeNode();
                queue.offer(cur.right);
                queue.offer(mid + 1);
                queue.offer(right);
            }
        }
        return root;
    }
}

image-20230707221808959

把二叉搜索树转换为累加树

538. 把二叉搜索树转换为累加树

其实这道题,如果思路一出来,代码是很简单的,但是主要还是先读懂题意,我们先把这道题先给转换成简单的一维数组。如果我要求数组

2 3 6

的一个倒序的累加,你要怎么求?是不是就是一个cur指针和一个pre指针,cur和pre的相加,然后替换cur的值,然后两个指针同时往前面移动。

2 3 6

2 9 6

11 9 6

那这道题也是一样的,也是需要利用双指针,从后面往前面遍历,你可以先看一下题目的遍历的顺序。

img

是不是右中左?是不是就是一个中序遍历?那这道题还不是轻轻松松就可以拿下了。

递归

class Solution {
    TreeNode pre = null;
    public TreeNode convertBST(TreeNode root) {
        if (root == null) return null;
        convertBST(root.right);
        root.val = pre == null ? root.val : pre.val + root.val;
        pre = root;
        convertBST(root.left);
        return root;
    }
}

image-20230708214707702

迭代

class Solution {
    public TreeNode convertBST(TreeNode root) {
        TreeNode pre = null;
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur != null) {
                if (cur.left != null) stack.push(cur.left);
                stack.push(cur);
                stack.push(null);
                if (cur.right != null) stack.push(cur.right);
            } else {
                cur = stack.pop();
                cur.val = pre == null 
                            ? cur.val
                            : cur.val + pre.val;
                pre = cur;
            }
        }
        return root;
    }
}

image-20230708215337282

回溯

组合问题

77. 组合

组合一

对于第一次遇到这种题,还是没有什么思路的,即使知道是用递归来做,但是却不知道怎么去写书,虽然前面写了挺多的二叉树的回溯了,但是感觉会还是有点不一样。所以这题,先看一看题解。

class Solution {
    //结果集
    List<List<Integer>> res = new ArrayList<>();
    //存放走过的路径
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracking(n, k, 1);
        return res;
    }

    private void backTracking(int n, int k, int startIndex) {
        //边界条件,表示已经收集好了两个值了
        if (list.size() >= k) {
            //那么就需要复制一个,然后放入结果集中
            res.add(new ArrayList<>(list));
            return;
        }

        //从上面来的第一个结点开始,然后往后面找组合的
        for (int i = startIndex; i <= n; ++i) {
            //加入根节点1,
            list.add(i);
            //由这一句递归函数,完成对1的其他组合的查找(根节点1,然后其他应该就从1+1开始找)
            backTracking(n, k, i + 1);
            list.remove(list.size() - 1);
        }
    }
}

image-20230708225546536

剪枝优化

这个优化的思路也很简单,就是说,你需要k个组合的数字,但是你剩下需要遍历的数组的数量不足需要的数量,那么就可以直接停了。

那么也就是还需要的数量<=剩下的数量。可以推出

k-list.size() <= n - i + 1

所以最后的代码可以优化为

class Solution {
    //结果集
    List<List<Integer>> res = new ArrayList<>();
    //存放走过的路径
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracking(n, k, 1);
        return res;
    }

    private void backTracking(int n, int k, int startIndex) {
        //边界条件,表示已经收集好了两个值了
        if (list.size() >= k) {
            //那么就需要复制一个,然后放入结果集中
            res.add(new ArrayList<>(list));
            return;
        }

        //如果剩下的不够数量,那么直接不继续遍历了,没必要,那么就直接回溯
        for (int i = startIndex; i <= n + 1 - k + list.size() ; ++i) {
            //加入根节点1,
            list.add(i);
            //由这一句递归函数,完成对1的其他组合的查找(根节点1,然后其他应该就从1+1开始找)
            backTracking(n, k, i + 1);
            list.remove(list.size() - 1);
        }
    }
}

image-20230708232700381

组合总和三

216. 组合总和 III

这道题和前面的那道题的思路是一样的,最大的变化就是什么时候需要收集结果。当走过的路径的总和达到n的时候,并且list的大小==k的时候,就可以收集了,而怎么获得路径的总和呢?这个就和前面的二叉树的回溯就很像了,直接通过一个变量,通过参数传递即可。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        combinationSum3(k, n, 1, 0);
        return res;
    }

    public void combinationSum3(int k, int n, int startIndex, int sum) {
        //剪枝操作,如果走过的路径已经是k个了,还没满足的话就超过个数了,
        if (k == list.size()) {
            //条件需要两个都满足才能加入结果
            if (sum == n) res.add(new ArrayList<>(list));
            return ;
        }

        //如果sum已经比n大了,那么你再加1~9任何一个,都不可能等于n了.所以直接退出.
        if (sum > n) {
            return ;
        }
        for (int i = startIndex; i <= 9; ++i) {
            list.add(i);
            combinationSum3(k, n, i + 1, sum + i);
            list.remove(list.size() - 1);
        }
    }
}

image-20230709231712831

还可以继续剪枝,1~9最多k就是9个,然后需要满足以下的式子

k - list.size <= 9 - i + 1
还需要的个数 <= 剩余的个数
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        combinationSum3(k, n, 1, 0);
        return res;
    }

    public void combinationSum3(int k, int n, int startIndex, int sum) {
        //剪枝操作,如果走过的路径已经是k个了,还没满足的话就超过个数了,
        if (k == list.size()) {
            //条件需要两个都满足才能加入结果
            if (sum == n) res.add(new ArrayList<>(list));
            return ;
        }

        //如果sum已经比n大了,那么你再加1~9任何一个,都不可能等于n了.所以直接退出.
        if (sum > n) {
            return ;
        }
        for (int i = startIndex; i <= 9 - k ; ++i) {
            list.add(i);
            combinationSum3(k, n, i + 1, sum + i);
            list.remove(list.size() - 1);
        }
    }
}

电话号码的字母组合

17. 电话号码的字母组合

这题和上面的一模一样,只不过多了一个数字和映射的关系,其实只要先把映射关系写出来,那么这道题就迎刃而解。

class Solution {

    // 先声明映射关系
    String[] map = {
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };
    List<String> res = new ArrayList<>();
    //最后结果的退出条件
    int len = 0;
    public List<String> letterCombinations(String digits) {
        len = digits.length();
        if (len == 0) return res;
        letterCombinations("", 0, digits);
        return res;
    }

    public void letterCombinations(String str, int startIndex, String digits) {
        if (str.length() == len) {
            res.add(str);
            return ;
        }

        //'2'=50
        for (char c: map[digits.toCharArray()[startIndex] - 48].toCharArray()) {
            //这里偷懒利用了string不可变的特性,就不需要写回溯代码
            letterCombinations(str + c, startIndex + 1, digits);
        }
    }
}

image-20230711205036189

如果为了效率,可以采用StringBuilder。

组合总和

39. 组合总和

这上面都是一类题型,如果不用考虑剪枝的话,代码是可以很快地写出来的

class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        combinationSum(candidates, target, 0, 0, path);
        return res;
    }

    private void combinationSum(int[] candidates, int target, int startIndex, int sum, List<Integer> list) {
        if (sum >= target) {
            if (sum == target) res.add(new ArrayList<>(list));
            return ;
        }

        for (int i = startIndex; i < candidates.length; ++i) {
            list.add(candidates[i]);
            combinationSum(candidates, target, i, sum + candidates[i], list);
            list.remove(list.size() - 1);
        }
    }
}

image-20230711210441889

发现我代码中写的一个剪枝其实有一点低效,因为当前sum > target后,我退出当前递归,如果后面的值都比前面大的话,又走到了循环,循环中又是走了一次sum > target的递归,所以,我们其实把判断条件放在for 循环中就可以更加高效了。

所以我们可以先进行一次排序,然后再剪枝

class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        combinationSum(candidates, target, 0, 0, path);
        return res;
    }

    private void combinationSum(int[] candidates, int target, int startIndex, int sum, List<Integer> list) {

        if (sum == target) {
            res.add(new ArrayList<>(list));
            return ;
        }

        for (int i = startIndex; i < candidates.length && target >= sum + candidates[i] ; ++i) {
            list.add(candidates[i]);
            combinationSum(candidates, target, i, sum + candidates[i], list);
            list.remove(list.size() - 1);
        }
    }
}

image-20230711211549988

最后的效率差不多,其实通过时间复杂度分析,如果数据量非常大的情况下,排序后的效果应该会比较好的。

组合总和II

40. 组合总和 II

如果不能重复,那么就直接暴力用Set解决即可。

class Solution {

    Set<List<Integer>> res = new HashSet<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        combinationSum2(candidates, 0, path, target, 0);
        return new ArrayList<>(res);
    }

    private void combinationSum2(int[] candidates, int startIndex, List<Integer> list, int target, int sum) {
        if (sum == target) {
            res.add(new ArrayList<>(list));
            return;
        }

        for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; ++i) {
            list.add(candidates[i]);
            combinationSum2(candidates, i + 1, list, target, sum + candidates[i]);
            list.remove(list.size() - 1);
        }
    }
}

果不其然,超时了。

image-20230711212400292

但是很简单,因为我们是排序过后了,所以一样的数字都在左右,我们直接记录前一个数字,然后跳过即可,然后就可以不用使用set集合了。

class Solution {

    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        combinationSum2(candidates, 0, path, target, 0);
        return(res);
    }

    private void combinationSum2(int[] candidates, int startIndex, List<Integer> list, int target, int sum) {
        if (sum == target) {
            res.add(new ArrayList<>(list));
            return;
        }
        int pre = 0;
        for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; ++i) {
            if (pre == candidates[i]) {
                continue ;
            }
            pre = candidates[i];
            list.add(candidates[i]);
            combinationSum2(candidates, i + 1, list, target, sum + candidates[i]);
            list.remove(list.size() - 1);
        }
    }
}

image-20230711213005532

分割回文串

*131. 分割回文串

这道题,如果没有见过,其实也是很难想到也是利用回溯来解决的,其实这道题也一样都是回溯,而且基本和上面的题一模一样,就是其中加了一个判断的条件,回文判断。

class Solution {
    List<List<String>> res = new ArrayList<>();
    List<String> list = new ArrayList<>();
    public List<List<String>> partition(String s) {
        partition(0, s);
        return res;
    }

    private void partition(int startInex, String s) {
        if (startInex == s.length()) {
            res.add(new ArrayList<>(list));
            return ;
        }

        for (int i = startInex; i < s.length(); ++i) {
            String temp = s.substring(startInex, i + 1);
            if (isTrue(temp)) {
                list.add(temp);
                //注意这里,如果有一个不是回文,那么就没有必要往下
            } else {
                continue;
            }
            partition(i + 1, s);
            list.remove(list.size() - 1);
        }
    }

    private boolean isTrue(String str) {
        char[] cArray = str.toCharArray();
        for (int i = 0, n = cArray.length; i < n / 2; ++i) {
            if (cArray[i] != cArray[cArray.length - i - 1]) 
                return false;
        }
        return true;
    }
}

复原IP地址

*93. 复原 IP 地址

思路还是和以前的一样,所以这道题的核心还是在什么时候终结条件,这道题是判断是否是有效的ip的地址,通过题目我们可以知道,需要有3个.,还要不能有前导0,还需要在0~255之间,这些都需要进行判断。这里附上我的第一版代码,挺丑的,但是还是过了,没有涉及到剪枝,而且有很多重复的地方。

class Solution {

    int len = 0;
    List<String> res=  new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {
        len = s.length();
        restoreIpAddresses("", s, 0);
        return res;
    }
    private void restoreIpAddresses(String path, String s, int startIndex) {
        if (startIndex >= s.length()) {
            if (isIp(path)) {
                res.add(path.substring(0, path.length() - 1));
            }
            return ;
        }
        for (int i =startIndex; i < s.length(); ++i) {
            //这个写法和回文串的一样.
            restoreIpAddresses(path + s.substring(startIndex, i + 1) + ".", s, i + 1);
        }
    }

    private boolean isIp(String ip) {

        //把最后一个的.去掉
        if (ip.charAt(ip.length() - 1) == '.') {
            ip = ip.substring(0, ip.length() - 1);
        } else {
            return false;
        }
        //如果不是三个.的话,直接失败
        if (len + 3 != ip.length()) {
            return false;
        }
                // System.out.println(ip);

        String[] nums = ip.split("\\.");
        for (String num: nums) {
            //没有前导0
            if (num.length() > 1 && num.charAt(0) == '0') return false;
            //这里要用long,避免溢出
            Long i = Long.parseLong(num);
            if (i > 255) return false; 
        }

        return true;
    }
}

image-20230713210437152

只是加了一句简单的剪枝,效率就明显地提高了。

    private boolean isTrue(String ip) {
        String[] nums = ip.split("\\.");
        if (nums.length > 4) return false;
        return true;
    }

        for (int i =startIndex; i < s.length(); ++i) {
            String temp = path + s.substring(startIndex, i + 1) + ".";
            if (!isTrue(temp)) continue;
            //这个写法和回文串的一样.
            restoreIpAddresses(temp, s, i + 1);
        }

image-20230713210714719

看了题解,思路还是一样的,果然还是核心的ip有效判别的差异了。他的判断数字是否大于255很优雅,每次进入的时候,只需要判断最新的数字,而是从头到尾从新判断。

子集

78. 子集

一看,这道题,完了不是组合问题, 那要怎么办呢,其实当你把那颗回溯的树一画出来,所有问题都迎刃而解了,这个题目,就不需要判断特定的条件去添加了,只需要每走过一个路径,都去添加即可。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        subsets(nums, 0);
        return res;
    }
    private void subsets(int[] nums, int startIndex) {
        res.add(new ArrayList<>(path));
        if (path.size() == nums.length) return;
        for (int i = startIndex; i < nums.length; ++i) {
            path.add(nums[i]);
            subsets(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

image-20230713215445021

子集II

90. 子集 II

因为不能重复,前面有一道组合总和还记得吗,我们对其进行排序,便可以跳过相同的值了。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        subsets(nums, 0);
        return res;
    }
    private void subsets(int[] nums, int startIndex) {
        res.add(new ArrayList<>(path));
        if (path.size() == nums.length) return;
        int pre = -11;
        for (int i = startIndex; i < nums.length; ++i) {
            if (pre == nums[i]) continue;
            pre = nums[i];
            path.add(nums[i]);
            subsets(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

image-20230713220206149

递增子序列

491. 递增子序列

这个的去重倒没有思路,所以首先还是利用set直接暴力去重,对于递增,就需要去记录一下上一个的值。

class Solution {
    Set<List<Integer>> res = new HashSet<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        findSubsequences(nums, -101, 0);
        return new ArrayList<>(res);
    }   

    private void findSubsequences(int[] nums, int pre, int startIndex) {
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
        }
        if (startIndex == nums.length) return;

        for (int i = startIndex; i < nums.length; ++i) {
            if (nums[i] < pre) continue;
            path.add(nums[i]);
            findSubsequences(nums, nums[i], i + 1);
            path.remove(path.size() - 1);
        }
    }
}

image-20230713221506592

通过题解,我们很快就能清楚一个事情,重复的集合,只会出现在同一层,所以我们只需要对同一层的数字进行去重即可。这里因为数字规定在-100到100,所以我们直接利用数组进行去重。

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

    public List<List<Integer>> findSubsequences(int[] nums) {
        findSubsequences(nums, -101, 0);
        return res;
    }   

    private void findSubsequences(int[] nums, int pre, int startIndex) {
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
        }
        if (startIndex == nums.length) return;

        //只有同一层的切割下,才会出现重复情况
        int[] hashSet = new int[220];
        for (int i = startIndex; i < nums.length; ++i) {
            //如果比前面的还小,就跳过
            if (nums[i] < pre) continue;
            //注意加100,
            //如果出现过这个数字,表示同层切割重复,跳过
            if (hashSet[nums[i] + 100] == 1) continue;
            hashSet[nums[i] + 100] = 1;
            path.add(nums[i]);
            findSubsequences(nums, nums[i], i + 1);
            path.remove(path.size() - 1);
        }
    }
}

image-20230713222615613

全排列

46. 全排列

这道题,和上面的那道题是差不多的思路,都是要记录之前出现过的值,上面那题排除的是当前层,而这道题排除的是当前遍历的路径。我们可以利用已经存放的path集合进行判断,但是他是一个o(n),所以我们可以采用数组的形式,直接o(1)。

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

    int[] cnt = new int[25];
    public List<List<Integer>> permute(int[] nums) {
        permute(nums, 0);
        return res;
    }   
    private void permute(int[] nums, int startIndex) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return ;   
        }

        for (int i = startIndex; i < nums.length; ++i) {
            //如果遍历的路径之前有过这个数字,就不要再进行填写
            if (cnt[nums[i] + 10] != 0) continue;
            path.add(nums[i]);
            //o(1)维护走过的路径
            cnt[nums[i] + 10] = 1;
            //注意这里从0开始,因为是全部数字都需要
            permute(nums, 0);
            path.remove(path.size() - 1);
            cnt[nums[i] + 10] = 0;
        }
    }
}

image-20230714235114707

全排列 II

47. 全排列 II

这道题则整合两种题的一个思路,一个是不重复,一个是上面的记录路径。不重复就是排序加pre结点,记录路径就是额外空间来一个记录路径的。

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

    int[] cnt = new int[25];
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        permute(nums, 0);
        return res;
    }   
    private void permute(int[] nums, int startIndex) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return ;   
        }

        int pre = -11;
        for (int i = startIndex; i < nums.length; ++i) {
            //如果遍历的路径之前有过这个数字,就不要再进行填写
            if (cnt[i] != 0) continue;
            if (pre == nums[i]) continue;
            pre = nums[i];
            path.add(nums[i]);
            //o(1)维护走过的路径
            cnt[i] = 1;
            //注意这里从0开始,因为是全部数字都需要
            permute(nums, 0);
            path.remove(path.size() - 1);
            cnt[i] = 0;
        }
    }
}

image-20230714235833173

*重新安排行程

N皇后

*51. N 皇后

其实这道题不算是很难,重要的是如何判断能被攻击是关键,所以当你解决了这个问题,其实这道题也就解决了其实。代码一开始还是比较抽象的,但还是过了。

class Solution {

    //判断当前行是否可以存放
    int[] line = new int[10];
    //判断当前列是否可以存放
    int[] column = new int[10];
    //判断当前位置的斜线位置是否可以存放
    int[][] cnt;
    List<List<String>> res = new ArrayList<>();
    List<String> path = new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        cnt = new int[n][n];
        solveNQueens0(n);
        return res;
    }

    private void solveNQueens0(int n) {
        //当路径走到了n个,就可以
        if (path.size() == n) {
            res.add(new ArrayList<>(path));
            return ;
        }

        //行遍历
        for (int i = 0; i < n; ++i) {
            //如果当前行已经被占领了.
            if (line[i] == 1) continue;
            //注意这里!如果当前行的上一行,没有可以落子的地方,直接跳过
            int temp = i - 1;
            if (temp >= 0 && line[temp] == 0) continue;

            //遍历列
            for (int j = 0; j < n; ++j) {
                //当前列已经被占领了.
                if (column[j] == 1) continue;

                boolean flag = false;
                //判断左上斜线
                for (int k = i - 1,  r = j - 1; k >= 0 && r >= 0; --k, --r) {
                    if (cnt[k][r] != 0) {
                        flag = true;
                        break;
                    }
                }
                if (flag) continue;

                //右上
                for (int k = i - 1,  r = j + 1; k >= 0 && r < n; --k, ++r) {
                    if (cnt[k][r] != 0) {
                        flag = true;
                        break;
                    }
                }
                if (flag) continue;

                //左下
                for (int k = i + 1, r = j - 1; k < n && r >= 0; ++k, --r) {
                    if (cnt[k][r] != 0) {
                        flag = true;
                        break;
                    }
                }
                if (flag) continue;

                //右下
                for (int k = i + 1,  r = j + 1; k < n && r < n; ++k, ++r) {
                    if (cnt[k][r] != 0) {
                        flag = true;
                        break;
                    }
                }
                if (flag) continue;

                //拼接字符串
                String str = "";
                for (int k = 0; k < j; ++k) {
                    str += ".";
                }
                str += "Q";
                for (int k = 0; k < n - j - 1; ++k) {
                    str += ".";
                }
                //标记
                line[i] = 1;
                column[j] = 1;    
                cnt[i][j] = 1;   
                path.add(str);
                solveNQueens0(n);
                path.remove(path.size() - 1);
                //移除标记
                column[j] = 0;
                line[i] = 0;
                cnt[i][j] = 0;
            }

        }
    }
}

image-20230715230703911

看了题解后,发现人家的思路是多么地清晰,判断斜线、遍历每一层,真的优雅。

判断斜线的时候,你可以观察规律,正的45的时候,斜线上的位置是不是相加都是一样的,135度的时候,是不是相减都是一样的,所以我们可以像行列一样声明两个数组进行标记。

而对于遍历每一层,其实他只是提供了一个层数的变量即可,其他都不需要提供。

class Solution {
    List<List<String>> res = new ArrayList<>();
    List<String> path = new ArrayList<>();
    int[] line = new int[10];
    int[] column = new int[10];
    int[] zheng = new int[20];
    int[] fan = new int[20];
    public List<List<String>> solveNQueens(int n) {
        solveNQueens(n, 0);
        return res;
    }   

    private void solveNQueens(int n, int row) {
        if (path.size() == n) {
            res.add(new ArrayList<>(path));
            return ;
        }

        for (int i = 0; i < n; ++i) {
            if (line[row] != 0 
            || column[i] != 0
            || zheng[row + i] != 0
            || fan[row - i + n + 1] != 0) continue;

            line[row] = 1;
            column[i] = 1;
            zheng[row + i] = 1;
            fan[row - i + n + 1] = 1;

            String str = "";
            for (int k = 0; k < i; ++k) {
                str += ".";
            }   
            str += "Q";
            for (int k = 0; k < n - i - 1; ++k) {
                str += ".";
            }
            path.add(str);
            solveNQueens(n, row + 1);
            path.remove(path.size() - 1);

            line[row] = 0;
            column[i] = 0;
            zheng[row + i] = 0;
            fan[row - i + n + 1] = 0;
        }
    }
}

image-20230716001337970

解数独

*37. 解数独

这道题其实和N皇后差不多,但是又有一点不一样,N皇后他是一维的,而解数独是二维的,并且数字还有1~9,所以你可以认为这里有三维之多,所以你需要遍历行,遍历列,还需要遍历数字。

class Solution {
    //用来判断是否重复的关键数组
    //行
    int[][] line = new int[15][255];
    //列
    int[][] column = new int[15][255];
    //九宫格
    int[][][] cnt = new int[3][3][255];

    public void solveSudoku(char[][] board) {
        //先初始化一次
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] != '.') {
                    line[i][board[i][j]] = 1;
                    column[j][board[i][j]] = 1;
                    cnt[i / 3][j / 3][board[i][j]] = 1;
                }
            }
        }
        solveSudoku0(board);
    }

    private boolean solveSudoku0(char[][] board) {
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] != '.') continue;
                for (int k = '1'; k <= '9'; ++k) {
                    if (line[i][k] != 0
                    || column[j][k] != 0
                    ||cnt[i / 3][j / 3][k] != 0) continue;
                    line[i][k] = 1;
                    column[j][k] = 1;
                    //获取第几个格子,然后和桶排一样把k放进去
                    cnt[i / 3][j / 3][k] = 1;
                    board[i][j] = (char)k;
                    if (solveSudoku0(board)) {
                        return true;
                    }
                    board[i][j] = '.';
                    line[i][k] = 0;
                    column[j][k] = 0;
                    cnt[i / 3][j / 3][k] = 0;
                }   
                return false;
            }
        }
        return true;
    }
}

image-20230716092228461

贪心算法

分发饼干

455. 分发饼干

贪心的问题,我认为很像模拟的一个过程,你要如何去满足最多人呢?是不是就是先把饼干喂给最合适的人,而且要从最小的饼干开始分配,这样才能最好的满足更多的人。那么你只要跟着这个思路把代码写出来就行了。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        int cnt = 0;
        //需要从最小的开始
        Arrays.sort(g);
        Arrays.sort(s);
        int lastIndex = 0;
        for (int need: g) {
            //从lastIndex开始,因为之前的小饼干一定满足不了后面的大人
            //而且需要剔除已经分配了的饼干.
            for (int i = lastIndex; 
                    i < s.length; 
                        ++i) {
                if (s[i] >= need) {
                    ++cnt;
                    lastIndex = i + 1;
                    break;
                }
            }
        }
        return cnt;
    }
}

image-20230716193123721

摆动序列

376. 摆动序列

这道题,坑点太多了,一般考虑的很难一次性把所有的情况都考虑完。

  1. 上下平坡:平坡需要计算一个
  2. 首尾元素:prediff和curdiff需要三个元素,而如果当只有两个元素的时候,需要特殊判断,或者模拟一个第零个元素,和第一个元素一样。
  3. 单调平坡:1 2 2 2 3 4 5 这样只有一个摆动序列的长度。

对于上下平坡,我们只需要记录prediff和curdiff,如果prediff大于等于0,然后curdiff小于0即可长度++,而prediff小于等于0,然后curdiff大于0即可长度++。

对于首尾元素,我们添加第零个元素,使prediff满足上面上下平坡的规则即可。

对于单调平坡,我们需要修改一下上下平坡的规则,使得同时满足两种情况。我们没必要每次都进行更新prediff,这样如果到了平坡,prediff就无法记录最开始进入平坡时的坡度情况,所以我们可以当遇到上下坡的时候,才去更新prediff。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        //初始化prediff,延长第零元素
        int preDiff = 0;
        int curDiff = 0;
        //对于末尾一个元素,默认就是一个坡,然后通过延长第零元素,判断第一个元素是否满足
        int cnt = 1;
        //这里只需要遍历到倒数第二个
        for (int i = 0;i < nums.length - 1; ++i) {
            //更新curdiff
            curDiff = nums[i + 1] - nums[i];
            //平坡问题
            if ((preDiff >= 0 && curDiff < 0) || (preDiff <= 0 && curDiff > 0)) {
                ++cnt;
                //当有转折点才去更新prediff,这样就能使prediff记录上下坡情况.
                preDiff = curDiff;
            }
        }
        return cnt;
    }
}

image-20230717004626887

最大子序和

*53. 最大子数组和

这道题,一看就很像是用滑动窗口来解题的,但是有一个更巧妙的滑动窗口优化是针对这道题来说的,最大的子数组和,那么是不是当当前的sum为负数的时候,他一定是起到了一个拖累后面成为最大子数组和的一个值,因为下一个值不管是什么值,最后加起来一定是小于下一个值!那么是不是就可以当检测到负数的时候,直接跳到下一个重新开始累加呢?

其实这样的思路就很像是滑动窗口,但是又不像是滑动窗口,滑动窗口还有一个类似回溯的思想,但是这道题我直接跳过,把所有的sum重置即可,直接跳到end指针,然后开始重新累加!

class Solution {
    public int maxSubArray(int[] nums) {
        int maxSum = Integer.MIN_VALUE;
        int sum = 0;
        //这里我们只需要维护一个指针
        for (int end = 0; end < nums.length; ++end) {
            //当前值加上去
            sum += nums[end];
            //先记录,即使跳过sum负数,但是最后最大的子数组和也有可能就是第一个负数的值
            maxSum = Math.max(sum, maxSum);
            //这个while是找到第一个非负数的,找的过程都需要记录最大值,比如-2 -1 序列的最大和是-1
            while (sum <= 0 && end < nums.length - 1) {
                //++end表示是当前end的下一个
                sum = nums[++end];
                maxSum = Math.max(sum, maxSum);
            }
        }
        return maxSum;
     }
}

image-20230717204950293

你看这个代码模板,像不像滑动窗口。滑动窗口是两个指针,而这个只是一个指针。

买卖股票的最佳时间||

*122. 买卖股票的最佳时机 II

其实这道题需要一点点的数学思维就可以很快地做出来了。你要卖股票,是不是只有当两天的价钱差为正的你才卖出是不是,那么你就计算数组每两天之间的差值,然后把所有正的全部加起来,那么就是最后卖出的价钱了。

class Solution {
    public int maxProfit(int[] prices) {
        int sum = 0;
        for (int i = 1; i < prices.length; ++i) {
            if (prices[i] - prices[i - 1] > 0) {
                sum += prices[i] - prices[i - 1];
            }
        }
        return sum;
    }
}

image-20230717211020800

跳跃游戏

*55. 跳跃游戏

这道题,真就是缝缝补补又三年。这道题的关键不是到某个结点后,判断我需要跳几格,而是每个格子能跳到哪里,跳到的最远处和一开始的范围中,其他结点又最远能跳到哪里,也就是一个范围的前进,像一个滑动窗口一样,以一定的范围往前走,直到能把最后结点覆盖为止。

//第一版代码,缝补出来的代码.
class Solution {
    public boolean canJump(int[] nums) {
        //只有一个结点,直接到了
        if (nums.length == 1) return true;
        //最后的位置
        int len = nums.length - 1;
        //结点范围中,其他结点能覆盖最远的范围
        int max = nums[0];
        //最远范围结点的位置
        int maxIndex = 0;
        for (int i = 0; i < nums.length - 1; ) {
            //当前结点能跳多远
            int jump = nums[i];
            //如果已经可以覆盖到最后一个,true
            if (maxIndex + max >= len) {
                return true;
            }

            //当前结点跳跃覆盖范围的所有节点中,覆盖范围最远的.
            int maxLen = 0;
            for (int j = i + 1; j < jump + i + 1; ++j) {
                //这个判断是,作为下一个遍历的结点,你首先要跑出上一个结点的覆盖范围
                if (nums[j] > jump + i - j) {
                    //找到最远的
                    if (nums[j] - jump - i + j > maxLen) {
                        maxIndex = j;
                        max = nums[j];
                        maxLen = nums[j] - jump - i + j ;
                    }
                }
            }

            //如果找不到,那就直接退出
            if (i == maxIndex) {
                return false;
            }
            //找到了就直接从新结点开始
            i = maxIndex;
        }
        return false;
    }
}

面向测试样例编程,从167的测试样例,缝补到最后通过。

image-20230717231700619

看了题解后,只能打出一个问号,还是很难理解的,但是其实思路差不多,只不过一些细节处理很优雅,但是核心思路还是一个覆盖范围的问题,能否在遍历的时候,取出被覆盖的结点的最远覆盖,能覆盖到最后一个结点。

class Solution {
    public boolean canJump(int[] nums) {
        //特判
        if (nums.length == 1) return true;
        //覆盖范围,第一个覆盖0位置
        int cover = 0;
        for (int i = 0; i <= cover; ++i) {
            //这个和上面的思路一样,都是要找到最远覆盖
            cover = Math.max(cover, nums[i] + i);
            //如果最远覆盖覆盖到了最后一个
            if (cover >= nums.length - 1) return true;
        }
        return false;
    }
}

image-20230717235349188

跳跃游戏 II

45. 跳跃游戏 II

我是真的没有想到,我上面一题的跳跃游戏,最开始的思路,居然被我用在了这里,而且还很顺利地过了!!核心就是找到目前覆盖范围结点中,跳得最远的一个。所以,我们可以根据上面一题中,题解的代码,配合上这个思路,可以很简单地写出代码。

class Solution {
    public int jump(int[] nums) {
        //特判
        if (nums.length == 1) return 0;
        int cover = 0;
        int cnt = 0;
        for (int i = 0; i <= cover; ) {
            //更新最远的覆盖范围
            cover = Math.max(cover, nums[i] + i);
            //如果可以覆盖最后一个结点,直接退出
            if (cover >= nums.length - 1) return cnt + 1;
            //下面是来找能跳出当前范围,并且还是当前范围中能跳到最远一个结点.
            int maxCover = cover;
            int maxIndex = i;
            //遍历覆盖范围中的每一个
            for (int j = i + 1; j <= cover; ++j) {
                if (nums[j] + j > maxCover) {
                    maxCover = nums[j] + j;
                    maxIndex = j;
                }
            }
            //如果没有一个能跳出当前范围,能么应该失败,但是测试样例说保证每个都能成功.
            if (maxIndex == i) return cnt;
            //直接跳到目标结点
            i = maxIndex;
            ++cnt;
        }
        return cnt;
    }
}

image-20230718213250300

看了题解,人家居然只有一个循环!但是其实我虽然是两个循环,但是实际是On的时间复杂度。但是不得不说人家代码写得很巧妙。

*K次取反后最大化的数组和

1005. K 次取反后最大化的数组和

如果要让数组和最大,那么是不是就是把负数全部取反,然后正数变那些最小的即可。

那么可以先把数组先来一次排序,然后把前面的负数取反,然后找到最后最小的一个值,去进行取反,总体思路就是这样,一些细节就是对k的把控,k是比负数的个数多吗,还是比负数的个数小,或是比全部的数组的长度还要长等等。

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums);
        //遍历过程中,维护最小的正数,包括被取反的负数
        int min = Integer.MAX_VALUE;
        int minIndex = 0;
        int res = 0;
        for (int i = 0; i < k; ++i) {
            //如果当前的i值已经比数组总长还长的话,那么就继续对数组再来一次操作即可
            if (i >= nums.length) {
                res = largestSumAfterKNegations(nums, k - nums.length);
                return res;
            }
            //负数
            if (nums[i] < 0) {
                //取反并维护
                nums[i] = Math.abs(nums[i]);
                if (nums[i] < min) {
                    min = nums[i];
                    minIndex = i;
                }
            } 
            //正数
            else {
                //如果是最小的,就可以取反,
                if (nums[i] < min) {
                    //如果是奇数,那么就需要取反,而偶数就不需要
                    if ((k - i) % 2 == 1) {
                        nums[i] = -nums[i];
                    }
                } else {
                //如果不是最小的,那么表示前面的负数更小
                    if ((k - i) % 2 == 1) {
                        nums[minIndex] = -nums[minIndex];
                    }
                }
                break;
            }
        }
        int sum = 0;
        for (int i : nums) {
            sum += i;
        }
        return sum + res;
    }
}

image-20230718230909842

题解也是差不多的思路,但是他的排序比我的巧妙许多,他是直接对绝对值进行排序的,后面就不用去考虑一个正负数最小值的问题。

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        nums = IntStream.of(nums)
                    .boxed()
                    .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
                    .mapToInt(Integer::intValue)
                    .toArray();

        for (int i = 0; i < nums.length; ++i) {
            if (nums[i] < 0 && k > 0) {
                nums[i] = -nums[i];
                --k;
            }
        }
        if (k > 0 && k % 2 == 1) {
            nums[nums.length - 1] = -nums[nums.length - 1];
        }
        return Arrays.stream(nums).sum();
    }
}

image-20230719000110861

*加油站

*134. 加油站

感觉有种悟到的感觉,对于两个数组,很常见的一个思路就是查看当前两个数组的差,也就是当前位置的一个奖惩关系。这道题,两个数组相减,那么你就可以得到,走过这个加油站,你到底是亏了还是赚了。那么如果你是亏的,你就不可能从这里起点,你连下一个地方都过不去,但是如果你是赚的,那么你就可能可以从这里开始,但是不保证是可以走完的,所以可以利用另外一个变量,先去判断,这给的两个数组,到底有没有可能存在答案,就是先把奖惩关系全部相加看看是否大于等于0,如果有结果,才去找到哪一个作为起点。那为什么,他不需要从头遍历,就可以当走到最后的时候,就已经找到了start起点了呢。那你想一想,以下的一个例子

区间一 区间二 start

那么你现在有一个start指针,表示前面区间一+区间二是小于0的,这个没有问题吧,那么你现在走到最后,发现都是正数,那么结果就是start这个位置,为什么不用循环遍历,不可能会出现在区间二和区间一中间呢?如果在区间一二中间,表示区间二是大于0,因为start才会维持在那里,那两个区间相加为负数,但是区间二为正数,那么是不是表示区间一是负数呢?那区间一是负数,那么start指针是不是就是在区间的分隔线,那么又回到了我们认定的逻辑,当走区间一的时候,start指针就已经会把指针指向区间分隔点了。

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int totalSum = 0;
        int curSum = 0;
        int start = 0;
        for (int i = 0; i < gas.length; ++i) {
            curSum += gas[i] - cost[i];
            //两个循环整合在一起
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {
                curSum = 0;
                start = (i + 1) % gas.length;
            }
        }
        if (totalSum < 0) return -1;
        return start;
    }
}

image-20230719213655943

分发糖果

135. 分发糖果

这道题,如果你固定思维,想要一次性解决两边,那么这道题就会很复杂,但是如果你改变思维,先解决一边的平衡,然后再去解决另一边的平衡,这道题就会及其的简单,只要你自己简单模拟一下过程,就可以很轻松地写出来。

class Solution {
    public int candy(int[] ratings) {
        int sum = 1;
        int[] get = new int[ratings.length];
        get[0] = 1;
        for (int i = 1; i < ratings.length; ++i) {
            if (ratings[i] > ratings[i - 1]) {
                int cur = get[i - 1] + 1;
                sum += cur;
                get[i] = cur;
            } else if (ratings[i] == ratings[i - 1]) {
                int cur = 1;
                sum += cur;
                get[i] = cur;
            } else {
                get[i] = 1;
                sum += 1;
            }
        }

        for (int i = ratings.length - 2; i >= 0; --i) {
            if (ratings[i] > ratings[i + 1]) {
                if (get[i] > get[i + 1]) {
                    continue;
                }
                int cur = get[i + 1] + 1;
                sum = sum - get[i] + cur;
                get[i] = cur;
            }
        }
        return sum;
    }
}

image-20230719222923116

柠檬水找零

860. 柠檬水找零

之前说过,贪心其实很像一个模拟的一个过程,你只要把找零的过程自己模拟一下走一遍,你就可以用代码写出来,即使最后代码思路不是很优雅,但是一般也能跑出来。

对于找零,你收了一张5元的,那么不需要找零了,你收了10元,你就要找零5元,需要看看有没有,你收了20,你就要找零15元,你要看看你有没有一张10元和一张5元或者三张5元的,然后优先用哪个呢?当然是优先使用10元的找零,因为10元和20元都有局限性,5元可以满足任何场景。

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;
        for (int give: bills) {
            int needReturn = give - 5;
            if (needReturn == 0) {
                ++five;
                continue;
            } else if (needReturn == 5) {
                ++ten;
                --five;
            }else if (needReturn == 15) {
                if (ten > 0) {
                    --ten;
                    --five;
                } else {
                    five -= 3;
                }
            }
            if (ten < 0 || five < 0) {
                return false;
            }
        }
        return true;
    }

}

image-20230720201046177

*根据身高重建队列

*406. 根据身高重建队列

这道题,我只知道一定要用身高进行排序,但是用后面的值进行插入那部分还是比较难想到的,用身高进行倒序排序后,在后面的值,前面的值一定都比后面的大或者等于,所以后面ki的值,刚好就是他在数组中的位置。

所以我们先对身高进行排序,大的在前,如果相等的话,那么就根据k值进行排序,小的在前。最后我们再根据k值进行插入即可。

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people, (p1, p2) -> {
            return p2[0] - p1[0] == 0 ? 
                            p1[1] - p2[1] :
                            p2[0] - p1[0];
        });

        List<int[]> list = new LinkedList<>();
        for (int[] p: people) {
            list.add(p[1], p);
        }
        return list.toArray(new int[people.length][]);
    }
}

image-20230720203624037

用最少数量的箭引爆气球

452. 用最少数量的箭引爆气球

为数不多直接搓出来的题目,我们先假设里面的气球都是有序的情况下,我们应该怎么去计算需要多少个箭呢?

(1,6)(2,8)(7,12)(10,16)

其中,第一个气球和第二个气球有交集,第二和第三有交集,第三和第四有交集。

你先走第一气球,然后再走第二个气球,你会发现,这两个气球有交集,再走第三气球,发现第三个气球只和第二个气球有交集,但是没有和第一个气球有交集,也就是说第三个气球和一二气球的交集没有交集,这时候要怎么办?是不是就要射出一个箭,去打中一和二的交集部分呢,这时候就会有同学问了,那第三个气球没有交集,后面的气球你怎么保证就没有呢?哈哈,我们一开始已经假设了,气球都是有序的,也就是数组第一位有序,次位再第一位相同的情况下也有序,所以,第四个甚至后面的气球,是不会超过第三个气球的左边界的。那么现在继续走第三个气球,走第四个,发现,第四个也有交集,那么就直接取当前两个气球的交集,便于下次运算,已经走到头了,那么直接当前发射箭数+1即可了。

class Solution {
    public int findMinArrowShots(int[][] points) {
        //保证有序,这里不采用相减的方式,避免溢出
        Arrays.sort(points, (p1, p2) -> {
            if (p1[0] == p2[0]) {
                return Integer.compare(p1[1], p2[1]);
            } else {
                return Integer.compare(p1[0], p2[0]);
            }
        });

        int cnt = 0;
        //这里是记录上一个的交集
        int[] pre = points[0];
        for (int i = 1; i < points.length; ++i) {
            int[] cur = points[i];
            //没有交集
            if (pre[1] < cur[0]) {
                ++cnt;
                //那么上一个的最大的交集就是当前的气球
                pre = cur;
            } 
            //相交,那么就是中间部分
            else if (pre[1] < cur[1]) {
                pre = new int[]{cur[0], pre[1]};
            } 
            //包含关系,那么就是小的那一个
            else {
                pre = cur;
            }
        }
        return cnt + 1;
    }
}

image-20230720211700043

无重叠区间

435. 无重叠区间

其实这道题和上一道题——用最少数量的箭引爆气球的思路是一样的,核心都是排序完,找到重叠的地方。

这道题,我只需要排序之后,找到会重叠的部分,然后cnt++即可,然后移出哪一个重叠的是关键,目的是,移出掉右边界最远的一个。

数组会出现三种情况,一种是普通的重叠,一种是包含,最后一种是没有重叠。

对于重叠的,第一种我们需要移出最右边的即可,那么下一个区间,重叠的可能就更小;第二种我们需要移出包含的那一个区间,也就是大的那一个,保留小的,所以下一个区间也会更难以重叠。

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals, (a, b)->{
            return Integer.compare(a[0], b[0]);
        });

        int cnt = 0;
        int[] pre = intervals[0];
        for (int i = 1; i < intervals.length; ++i) {
            int[] cur = intervals[i];
            if (cur[0] < pre[1]) {
                pre = new int[]{pre[0], Math.min(cur[1], pre[1])};
                ++cnt;
            } else {
                pre = cur;
            }
        }
        return cnt;
    }
}

image-20230722233239150

划分字母区间

763. 划分字母区间

这道题的核心在于,你怎么知道后面就已经没有字母了,其实只需要一开始的时候用hashmap记录一下每个字母的数量即可。遍历的时候,如果是新字母就加上需要遍历的个数,如果不是新字母,那么就遍历次数--,新老字母就用一个hashset集合保存就好了。

class Solution {
    public List<Integer> partitionLabels(String s) {
        Map<Character, Integer> map = new HashMap<>(s.length());
        //
        for (char c: s.toCharArray()) {
            map.put(c, map.getOrDefault(c, 0) + 1);
        }

        int i = 0;
        int cnt = 0;
        int pre = 0;
        List<Integer> list = new ArrayList<>();
        Set<Character> set = new HashSet<>();
        do {
            char c = s.charAt(i);    
            if (!set.contains(c)) {
                int chNum = map.get(c);
                cnt += chNum;
                set.add(c);
            } 
            --cnt;
            if (cnt == 0) {
                list.add(i - pre + 1);
                pre = i + 1;
            }
            ++i;
        } while (i < s.length());
        return list;
    }
}

image-20230723010219412

发现题解的思路更加巧妙,他不是记录每个元素的出现次数,而是记录每个元素出现的最远的位置,然后每次遍历,只要更新最远的位置即可,等到遍历到了,那么就添加结果。

class Solution {
    public List<Integer> partitionLabels(String s) {
        int[] position = new int[30];
        for (int i = 0; i < s.length(); ++i) {
            position[s.charAt(i) - 'a'] = i;
        }

        List<Integer> list = new ArrayList<>();
        int pre = 0;
        int right = 0;
        for (int i = 0; i < s.length(); ++i) {
            //找到元素中最远的位置
            right = Math.max(position[s.charAt(i) - 'a'], right);
            if (i == right) {
                list.add(i - pre + 1);
                //记录上次的最远的位置
                pre = i + 1;
            }
        }
        return list;
    }
}

image-20230723231703796

合并区间

56. 合并区间

当你会了上面那些题目之后,你会发现这道题的思路也是一模一样的,很快就可以推导出来。

排序当然不用说了吧,这种题,就是要让他变为最紧凑的数组,然后遍历,找到重叠的区间,然后更新最远的范围即可,下一个就继续与最远的范围进行比较。

class Solution {
    public int[][] merge(int[][] intervals) {
        //特判
        if (intervals.length == 1) {
            return intervals;
        }
        //排序
        Arrays.sort(intervals, (a, b) -> {
            return Integer.compare(a[0], b[0]);
        });

        int[][] res = new int[intervals.length][];
        int cnt = 0;
        //用来存放上一个的最大的范围
        int[] pre = intervals[0];
        for (int i = 1; i < intervals.length; ++i) {
            //重叠
            if (intervals[i][0] <= pre[1]) {
                //更新最大的范围
                pre[1] = Math.max(intervals[i][1], pre[1]);
            } else {
                //如果不是重叠的,就要记录上一个重叠的最大范围,也就是已经合并了
                res[cnt++] = pre;
                pre = intervals[i];
            }
            //最后一个需要特殊处理,有可能是被合并了,也有可能是单独一个
            if (i == intervals.length - 1) {
                res[cnt++] = pre;
            }
        }
        int[][] a = new int[cnt][];
        int index = 0;
        for (int i = 0; i < cnt; ++i) {
            a[index++] = res[i];
        }
        return a;
    }
}

image-20230723234221440

*单调递增的数字

738. 单调递增的数字

这个题目你只要用几个数字自己去模拟一下即可,比如98,那么变为最大的递增是什么呢,是不是89,那么这个规律是什么呢,就是当前一位变为9,然后前一位减一就行。只不过要注意一点,就是如果你数字中前面有一个变成9,那么后面都需要变为9了,所以你需要一个记录位置的标志。

class Solution {
    public int monotoneIncreasingDigits(int n) {

        StringBuilder sb = new StringBuilder(n + "");
        int flag = sb.length();
        for (int i = sb.length() - 1; i > 0; --i) {
            if (sb.charAt(i) < sb.charAt(i - 1)) {
                 sb.setCharAt(i - 1, (char)((sb.charAt(i - 1) - 1)));
                flag = i;
            }
        }
        //System.out.println(flag);
        //System.out.println(sb.length());
        for (int i = flag; i < sb.length(); ++i) {
          //  System.out.println(i);
            sb.setCharAt(i, '9');
        }
        //System.out.pri    ntln(sb.toString());
        return Integer.parseInt(sb.toString());
    }
}

image-20230726004808641

监控二叉树

968. 监控二叉树

这道题还是很难,题目分析难,坑点多,状态转移乱,都是考虑很多的。首先,要知道什么时候放置摄像头,或者说什么时候一定不会放置摄像头,通过观察,我们可以知道,作为叶子结点,一定不可能放置摄像头的,因为这样你一定会浪费了监控的下一层的范围,所以一定是在叶子结点的父结点进行放置摄像头,所以从这里得知,我们需要从叶子结点往前去遍历,然后判断是否需要放置摄像头,所以得出需要使用后序遍历的结论。

那遍历结点,我们要怎么知道是否需要放置摄像头呢?是不是我们需要记录一下子结点的一个状态,才能去判断是否需要放置摄像头。

  1. 如果子结点只要有一个没有被监控到,那么该结点一定要放置摄像头。
  2. 如果子结点只要有一个放置了摄像头,那么该结点一定不需要放置摄像头,因为已经被监控到了。
  3. 那么就剩下两个子结点都被监控,那么该结点也不需要放置摄像头了。

所以通过上面得知,一个结点,有且仅有三种状态,0. 没有被监控到;1. 放置了摄像头;2. 被监控到了。

这里还有问题,如果我们遍历到空的结点,我们需要返回的是什么的一个状态呢?这里我们模拟一下,叶子结点的两个子结点都是为空,然后我们的目的是让叶子结点的父结点去放置摄像头。

  1. 如果空的时候返回0,没有被监控到,那么叶子结点是不是就一定要放置摄像头,不符合。
  2. 如果空的时候返回1,放置了摄像头,那么叶子结点放摄像头是不是就浪费了,不符合。
  3. 如果空的时候返回2,被监控到了,那么叶子结点发现两个子结点都被监控到了,那么当前的叶子结点也就不需要放置摄像头了,并且设置状态为没有被监控,所以叶子结点的父结点最终就会放置摄像头。

注意,最后还有一个坑,如果,遍历到最后根结点的时候,状态是没有被监控到呢,是不是遍历就会结束,而且也没有放置摄像头,因为他想要该节点的父结点去放置,但是他作为根结点他已经没有父结点了,所以我们需要额外判断这种情况。

class Solution {
    int cnt = 0;
    public int minCameraCover(TreeNode root) {
        int i = interval(root);
        //需要判断根结点是否会被监控到.
        if (i == 0) {
            ++cnt;
        }
        return cnt;
    }
    private int interval(TreeNode root) {
        //空的时候返回2已经被监控了
        if (root == null) {
            return 2;
        }
        int a = interval(root.left);
        int b = interval(root.right);
        //如果两个都是被监控了
        if (a == 2 && b == 2) {
            //设置当前结点没有被监控
            return 0;
        } 
        //如果只要有一个子结点是没有被监控,表示需要放置摄像头
        else if (a == 0 || b == 0) {
            ++cnt;
            return 1;
        }
        //如果有一个放置了摄像头,表示当前结点是被监控到了.
        else if (a == 1 || b == 1) {
            return 2;
        }
        return 0;
    }
}

image-20230728002926452

动态规划

斐波那契数

509. 斐波那契数

递归入门题目, 我们先直接声明一个dp数组来计算,从题目知道,要求出下一个的值,需要前面两个的值,所以我们就可以利用dp数组,来存放所有走过的值,对于初始化,n=0的时候,值为0,n=1的时候,值为1,那么有两个值后,就可以进行计算了。

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

image-20230728013143195

但是,我们发现,好像一直只需要使用到dp数组的两个值,那这样不是直接使用两个变量就完成了吗?

class Solution {
    public int fib(int n) {
        if (n == 0) return 0;
        int prepre = 0;
        int pre = 1;
        for (int i = 2; i <= n; ++i) {
            int temp = pre + prepre;
            prepre = pre;
            pre = temp;
        }
        return pre;
    }
}

image-20230728013433466

对于递推公式,其实我们可以直接使用递归来做这道题

class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        return f(n);
    }
    private int f(int n) {
        return fib(n - 1) + fib(n - 2);
    }
}

image-20230728013618149

爬楼梯

70. 爬楼梯

这题也是一样的思路,我们从dp数组,再到两个变量,最后递归的形式进行讲解。

你模拟一下前面几层楼梯的爬楼梯的方案数,你就很容易发现规律。

我走三层楼梯,我要不就是从一层爬上来,要不就从二楼爬上来,那么,是不是爬三层楼梯就是爬两层楼梯+爬一层楼梯的方案数呢?

我走四层楼梯,我要不就是从三层爬上来,要不就是二层爬上来,那么是不是就是爬四层就是爬三层+爬两层的方案数呢?而爬三层,又是一样的思路下去。

dp数组的方式。

class Solution {
    public int climbStairs(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;
        int[] dp = new int[n + 2];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; ++i) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

image-20230728204542287

两个变量的方式。

class Solution {
    public int climbStairs(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;
        int pre = 2;
        int prepre = 1;
        for (int i = 3; i <= n; ++i) {
            int cur = pre + prepre;
            prepre = pre;
            pre = cur;
        }
        return pre;
    }
}

image-20230728204423851

对于递归,我爬四层,是不是就是要计算爬第三层和爬第二层的,爬第三层是不是就是要计算爬第一层和第二层的,所以这也就是一个递归的过程。

class Solution {
    public int climbStairs(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;
        return climbStairs(n - 1) + climbStairs(n - 2);
    }
}

只不过居然超时了。在45这个测试样例超时了,对于45之前的样例都是没有问题的。

使用最小花费爬楼梯

746. 使用最小花费爬楼梯

这道题的题意还是有点模糊的,如果没有实例,我一直就以为楼顶就是cost的长度,原来他是还要再高一层。动态规划的核心思想,还是要记录一下我之前走过的最优解,然后每走一步,都是最优解即可。走到第三层,我就需要有第二层的体力消耗和第一层的体力消耗,然后找到体力消耗最小的,在那里开始爬。那么现在跳到第四层,我就需要有爬到第三层和爬到第二层的体力消耗,而爬到第三层的体力消耗需要有前面一二层的体力消耗加上第三层跳跃的体力消耗。

这道题是因为他已经给了一个数组了,所以可以直接利用他来作为我们的dp数组。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        //两个特判
        if (cost.length == 1) return cost[0];
        if (cost.length == 2) return Math.min(cost[0], cost[1]);

        for (int i = 2; i < cost.length; ++i) {
            //需要知道从哪里开始跳
            int curMin = Math.min(cost[i - 1], cost[i - 2]);
            //更新一下,下一层的上一层的体力消耗.
            cost[i] += curMin;
        }
        //到楼顶需要找到前两层的最小消耗.
        return Math.min(cost[cost.length - 1], cost[cost.length - 2]);
    }
}

image-20230728211158220

这里再写一个利用dp数组的方式,dp数组记录的就是到这一楼层需要消耗的体力。而上面那种解法是,跳到下一层楼需要的体力。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        if (cost.length == 1) return cost[0];
        if (cost.length == 2) return Math.min(cost[0], cost[1]);
        int[] dp = new int[cost.length + 1];
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <= cost.length; ++i) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        // System.out.println(Arrays.toString(dp));
        return dp[cost.length];
    }
}

image-20230728213004190

这里把dp数组变为变量

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int prepre = 0;
        int pre = 0;
        //注意这里是<=,因为还有最后登顶。
        for (int i = 2; i <= cost.length; ++i) {
            int cur = Math.min(prepre + cost[i - 2], pre + cost[i - 1]);
            prepre = pre;
            pre = cur;
        }
        return pre;
    }
}

image-20230819005114012

*不同路径

62. 不同路径

对于爬楼梯,dp数组记录的是,我走到这里需要耗费的体力,或者步数,对于这道题,dp数组记录的也是,我走到这里有多少种不同的路径。

对于一个二维数组,走到每一个格子的方式只有从上面或者从左边过来,所以到当前格子的路径总数就是要记录上面格子和左边格子的不同路径的和。

如果初始化呢?可以知道,其实两个边界,就是上边和左边的到达路径只有一条,所以可以优先对这两个边界进行初始化为1。

class Solution {
    public int uniquePaths(int m, int n) {
        //只有一列或者一边的情况,直接返回1种方式
         if (m == 1 || n == 1) return 1;
         int[][] dp = new int[m + 1][n + 1];
         //对上边和左边进行初始化
        for (int i = 0; i < m; ++i) {
            dp[i][0] = 1;
        }
        for (int i = 0; i < n; ++i) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                //核心递推公式,就是左边格子和上面格子的路径和
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
}

image-20230728220441318

不同路径 II

63. 不同路径 II

这道题其实就是在上面的那道题稍微变化一下,加入了一个障碍,那么加入障碍,那么当前格子就永远无法到达,那么路径就是为0,然后如果格子的左边或者上边是障碍,那么就只能有一边可以过来或者两边都过不来。

对于初始化边界,只要有一个障碍物,后面的格子都过不去了,前面的格子还是可以到达。

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        //遍历左边
        for (int i = 0; i < m; ++i) {
            //如果有障碍物
            if (obstacleGrid[i][0] == 1) {
                //那么下面的格子都无法到达
                for (int j = i; j < m; ++j) {
                    dp[j][0] = 0;
                }
                break;
            } else {
                dp[i][0] = 1;
            }
        }
        for (int i = 0; i < n; ++i) {
            if (obstacleGrid[0][i] == 1) {
                for (int j = i; j < n; ++j) {
                    dp[0][j] = 0;
                }
                break;
            } else {
                dp[0][i] = 1;
            }
        }

        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                //如果当前的格子是障碍物,直接无法到达
                if (obstacleGrid[i][j] == 1) {
                    dp[i][j] = 0;
                } else {
                    //如果上边没有障碍,就加上
                    if (obstacleGrid[i - 1][j] != 1) {
                        dp[i][j] += dp[i - 1][j];
                    }
                    //如果左边没有障碍,就加上
                    if (obstacleGrid[i][j - 1] != 1) {
                        dp[i][j] += dp[i][j - 1];
                    }
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[m - 1][n - 1];
    }
}

image-20230728223111774

把代码优雅一下,让他好看一点

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int[][] dp = new int[obstacleGrid.length][obstacleGrid[0].length];
        for (int i = 0; i < obstacleGrid.length; ++i) {
            if (obstacleGrid[i][0] == 0) {
                dp[i][0] = 1;
            } else {
                break;
            }
        }
        for (int i = 0; i < obstacleGrid[0].length; ++i) {
            if (obstacleGrid[0][i] == 0) {
                dp[0][i] = 1;
            } else {
                break;
            }
        }
        for (int i = 1; i < obstacleGrid.length; ++i) {
            for (int j = 1; j < obstacleGrid[0].length; ++j) {
                if (obstacleGrid[i][j] == 1) continue;
                if (obstacleGrid[i][j - 1] == 0) dp[i][j] += dp[i][j - 1];
                if (obstacleGrid[i - 1][j] == 0) dp[i][j] += dp[i - 1][j];
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[obstacleGrid.length - 1][obstacleGrid[0].length - 1];
    }
}

image-20230813224244760

*整数拆分

343. 整数拆分

一样,一起来先想dp数组的含义是什么,这道题要求整数拆分后乘积的最大化,那么当然保存的都是每一个数拆分后的乘积的最大化了。

对于一个整数的拆分方案,遍历的时候有两种拆分方法,一种是只拆一次,一种是一直拆。举个例子,对于n=6,拆33的时候是最大的,但是如果你对3继续拆的话,就会得到2 3 = 6的结果,所以对于当前值被拆分后需要判断两次,一次是只拆分成两个数,另一次是拆分成两个数以上的,就是需要用到dp数组的情况。

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n + 1];
        dp[2] = 1;
        //遍历3到n,记录一下3到n所有数的拆分后的情况
        for (int i = 3; i <= n; ++i) {
            //从1开始拆分
            for (int j = 1; j < i; ++j) {
                //拆成两个
                dp[i] = Math.max(j * (i - j), dp[i]);
                //拆分成3个及以上
                dp[i] = Math.max(j * dp[i - j], dp[i]);
            }
        }
        // System.out.println(Arrays.toString(dp));
        return dp[n];
    }
}

image-20230728233543192

*不同的二叉搜索树

96. 不同的二叉搜索树

动态规划还是一个找规律的题目,善于从题目的例子中找到规律,也就是找到递推公式,有了递推公式,难度就至少减少了一半,但是如果你连递推公式都找不到,这道题基本就是很难搞定。

我们画出n=1,2,3数量的不同二叉搜索树的结果。从2开始,可以看到,有1作为头结点,有2作为头结点,看3,分别有1、2、3作为头结点,所以至少需要三种数量的相加。看3为头结点,左边有两个节点,而且两种子树的布局和n=2的布局是一样的,1为头结点,右边有两个节点,而且两种子树的布局也和n=2的布局是一样的,2为头结点,左边的布局和n=1的布局是一致的,右边的布局也和n=1的布局是一致的。

所以我们可以举例出当n=3的时候

1为头结点 = 左子树0个结点*右子树2个结点

2为头结点= 左子树1个结点*右子树1个结点

3为头结点= 左子树2个结点*右子树0个结点

根据这里,那么递推公式也就明朗了起来。

class Solution {
    public int numTrees(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;
        int[] dp = new int[n + 1];
        //初始化
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; ++i) {
            //当n=i的时候,头结点分别为j的情况
            for (int j = 1; j <= i; ++j) {
                //dp[i]+=左子树j-1个结点*右子树i-j个结点
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
}

image-20230729200209202

*01背包

题目:有n种物品,每种物品都是重量和价值,而且每种物品只有一份,现在有一个容量一定的背包,你需要尽可能地装东西,使得背包价值最高。

比如这里,葡萄重量为2,价值为3,矿泉水重量为3,价值为5,西瓜重量为4,价值为6,背包最大容量为6,你现在就需要装东西,使得价值最大。

image-20230729210803969

  1. dp数组的含义:做这种题,我们一般先需要一个二维的dp数组,dp[i][j],dp数组的含义是存放前i种物品在j容量的背包下,最大的价值。
  2. 确认递推的公式:

以遍历背包容量为开始,比如说,遍历第一行第一列的时候,首先需要决定要不要装下这个葡萄,如何比较呢?是不是就是要比较装了葡萄后加上剩余容量可以装的最大价值和没有装葡萄的时候的最大价值作比较。

而我们知道,dp数组的含义是存放前i种物品在j容量的背包下,最大的价值,所以不装葡萄的的时候的最大价值就是dp[i-1][j],然后如果装了葡萄呢?就是要葡萄的价值,然后当前背包的容量减去葡萄的重量,剩下的容量的背包,可以装的最大价值,也就是dp[i-1][j-weight[i-1]],即dp[i - 1][j - weight[i - 1]] + value[i - 1]dp[i-1][j]作比较得出最大的价值,也就是当前dp[i][j]的值了。

//先看看能不能装下葡萄先
if (j < weight[i - 1]) {
    //不能当然就是之前的最大价值即可
    dp[i][j] = dp[i - 1][j];
} 
//可以装下葡萄,就需要计算装了葡萄后,是否值得.
else {
    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
  1. dp数组的初始化:第0行,表示没有装任何的物品,所以每一种容量的背包的价值都是0,第0列,0容量的背包,不可能装下任何东西,所以价值也都是0。

image-20230729211306284

  1. 遍历顺序:因为要遍历行和列,所以需要两个for循环,然后观察递推的公式,他dp的值依赖的是上方的值和左上方的值,所以对于两层for循环来说,先遍历背包还是先遍历物品都不会影响最终的结果,或者先顺序还是倒序也是没有影响的,因为核心就是在于,二维数组,他上下两层的数据是完全隔离的,当前层不会影响到上一层的结果。
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
        int[][] dp = new int[weight.length + 1][bagSize + 1];
        for (int i = 1; i <= weight.length; ++i) {
            for (int j = 1; j <= bagSize; ++j) {
                if (j < weight[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }
        System.out.println(Arrays.deepToString(dp));
    }
/*
[[0, 0, 0, 0, 0], 
[0, 15, 15, 15, 15], 
[0, 15, 15, 20, 35], 
[0, 15, 15, 20, 35]]
*/

然后这里再来讲一下二维dp数组怎么变为一维的dp数组,其实核心的思想就是把数组进行拷贝到当前层,观察二维dp的递推公式,他只需要用到上一层的数据,所以如果我们把上一层的数据拷贝到当前一层,对于赋值这种操作是没有影响的。

但是这样改,对于遍历顺序产生了一点影响,就是他如果继续按照上面的那种遍历的顺序,就会出现一种物品被多次存放的情况,因为二维的都是取i-1,所以不会包含当前的物品。所以这里我们需要怎么改呢?其实只要在遍历背包的时候,使用倒序遍历即可了,这样,他一直会修改最后的值,而比较判断的时候是读取前面的,所以不会产生问题。

而且我们只能先遍历物品,再遍历背包,我们来模拟一下,继续是背包倒序,从后面开始添加,先遍历123的物品,直接添加最高价值,且能装进去的物品;倒数第二个,也是一样的结果,直接添加最高价值,且能装进去的物品,所以这样遍历到最后,背包中只会有一个物品,所以也不符合题意。

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
        int[] dp = new int[bagSize + 1];
        for (int i = 1; i <= weight.length; ++i) {
            for (int j = bagSize; j >= 1; --j) {
                if (j >= weight[i - 1]) {
                    dp[j] = Math.max(dp[j], dp[j - weight[i - 1]] + value[i - 1]);
                }
            }
        }

        System.out.println(Arrays.toString(dp));
    }

/*
[0, 15, 15, 20, 35]
*/

然后对于这里的一维dp数组的写法还可以继续优化一下,因为你遍历背包顺序的时候,是从最大容量开始遍历的,所以如果第一个无法装下了,后面其实就不需要继续去比较了,所以我们直接把if的条件判断放在for循环里面即可。

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
        int[] dp = new int[bagSize + 1];
        for (int i = 1; i <= weight.length; ++i) {
            for (int j = bagSize; j >= weight[i - 1]; --j) {
                dp[j] = Math.max(dp[j], dp[j - weight[i - 1]] + value[i - 1]);
            }
        }

        System.out.println(Arrays.toString(dp));
    }

/*
[0, 15, 15, 20, 35]
*/

*分割等和子集

416. 分割等和子集

这道题,感觉可以直接暴力回溯,等和,所以就是sum的一半就行了。

class Solution {
    int sum;
    int presum = 0;
    boolean flag = false;
    public boolean canPartition(int[] nums) {
        sum = Arrays.stream(nums).sum();
        canPartition(nums, 0);
        return flag;
    }

    private void canPartition(int[] nums, int startIndex) {
        //当一半的时候退出就好了
        if (presum * 2 == sum) {
            flag = true;
            return ;
        }

        for (int i = startIndex; i < nums.length; ++i) {
            presum += nums[i];
            canPartition(nums, i + 1);
            presum -= nums[i];
        }
    }
}

果不其然,超时了。

image-20230730224809328

这章是动态规划中01背包的,但是对于根本没有接触过01背包的题目,我看着是很懵的。

看了题解一个关键的思路,就恍然大悟,关键就是背包容量就是sum / 2,如果最后物品刚好能填满背包,就表示说所有的数,能找到一个和(价值)为sum/2的一个组合。

二维dp

class Solution {
    public boolean canPartition(int[] nums) {
        //求出综合
        int bagSize = Arrays.stream(nums).sum();
        //如果是奇数,一定不可能能找到一半和的
        if (bagSize % 2 == 1) {
            return false;
        }
        bagSize /= 2;
        //先用二维dp数组来写一下,巩固两种做法
        int[][] dp = new int[nums.length + 1][bagSize + 1];
        for (int i = 1; i <= nums.length; ++i) {
            for (int j = 1; j <= bagSize; ++j) {
                if (j < nums[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //如果当前物品能放得下,需要看值不值得放下去
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i - 1]] + nums[i - 1]);
                }
            }
        }
        //看最后背包是否能装满sum/2
        return dp[nums.length][bagSize] == bagSize;
    }
}

image-20230730230851990

一维dp

class Solution {
    public boolean canPartition(int[] nums) {
        //求出综合
        int bagSize = Arrays.stream(nums).sum();
        //如果是奇数,一定不可能能找到一半和的
        if (bagSize % 2 == 1) {
            return false;
        }
        bagSize /= 2;
        //先用二维dp数组来写一下,巩固两种做法
        int[] dp = new int[bagSize + 1];
        for (int i = 1; i <= nums.length; ++i) {
            for (int j = bagSize; j >= 1; --j) {
                if (j >= nums[i - 1]) {
                    //如果当前物品能放得下,需要看值不值得放下去
                    dp[j] = Math.max(dp[j], dp[j - nums[i - 1]] + nums[i - 1]);
                }
            }
        }
        //看最后背包是否能装满sum/2
        return dp[bagSize] == bagSize;
    }
}

image-20230730231046693

优化一下

class Solution {
    public boolean canPartition(int[] nums) {
        //求出综合
        int bagSize = Arrays.stream(nums).sum();
        //如果是奇数,一定不可能能找到一半和的
        if (bagSize % 2 == 1) {
            return false;
        }
        bagSize /= 2;
        //先用二维dp数组来写一下,巩固两种做法
        int[] dp = new int[bagSize + 1];
        for (int i = 1; i <= nums.length; ++i) {
            for (int j = bagSize; j >= nums[i - 1]; --j) {
                //如果当前物品能放得下,需要看值不值得放下去
                dp[j] = Math.max(dp[j], dp[j - nums[i - 1]] + nums[i - 1]);
            }
        }
        //看最后背包是否能装满sum/2
        return dp[bagSize] == bagSize;
    }
}

image-20230730232448430

*最后一块石头的重量II

1049. 最后一块石头的重量 II

对于题目,我感觉不能陷入题目的固定思维中,你要想想,是要如何才能去让石头最后的重量最小,一碰一的情况下,是不是就是两个重量最相近,那多对多的情况下,是不是就是两份石头的重量最相近,那最相近意味着什么,其实就是一份石头分两份,每一份的重量最接近sum/2,那么最接近,是不是就是最大的重量,那是不是就是转换为,背包容量为sum/2,求最大容量?那最后把容量和总容量的中剩余的一减,最后的差不就是答案了吗。其实这道题的思路和上面一题的思路差不多,只不过需要把思路转换一下,变成一个背包问题,然后上面是求刚好二分之一,这里是最接近二分之一,更加贴近原始的01背包。

class Solution {
    public int lastStoneWeightII(int[] stones) {
        //求出总和,找到二分之一
        int sum = Arrays.stream(stones).sum();
        int bagSize = sum;
        bagSize /= 2;
        int[]dp = new int[bagSize + 1];
        for (int i = 1; i <= stones.length; ++i) {
            for (int j = bagSize; j >= stones[i - 1]; --j) {
                dp[j] = Math.max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
            }
        }

        //这是一半容量的最大装填
        int half = dp[bagSize];
        //需要是abs
        return Math.abs(sum - half * 2);
    }
}

image-20230731001053963

性能主要是使用了stream的求sum,过程会涉及到频繁的拆装箱,这里用手写的一下求sum。

class Solution {
    public int lastStoneWeightII(int[] stones) {
        //求出总和,找到二分之一
        int sum = 0;
        //这里还可以用增强for
        for (int i = 0; i < stones.length; ++i) {
            sum += stones[i];
        }
        //这里还可以使用位运算提高性能。
        int bagSize = sum / 2;
        int[]dp = new int[bagSize + 1];
        for (int i = 1; i <= stones.length; ++i) {
            for (int j = bagSize; j >= stones[i - 1]; --j) {
                dp[j] = Math.max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
            }
        }

        //这是一半容量的最大装填
        int half = dp[bagSize];
        //需要是abs
        return Math.abs(sum - half * 2);
    }
}

image-20230731001311743

**目标和

494. 目标和

一开始,看到要正负,然后还有target,也是和上面的题目一样,想到一个分类的,左集合和右集合,然后观察样例,和为5,要得到target为3的,发现只需要相加然后/2就可以找到左集合需要得到多少的数了,所以问题最后又回到了装满背包,但是核心问题就是,装满背包有多少种方法?这个就是很难想到的。

首先明确dp数组含义,首先直觉一定是装满该背包,有多少种方法,但是需要怎么求呢?可以这么想,比如说现在装满背包为3的方案总共有2种,那么你现在遍历到物品值为1,然后背包容量为4,那么装满背包的方案有多少种,是不是就是3种(注意不是3+1种),所以是不是就可以推出一个递推公式dp[j] += dp[j - nums[i]],也就是原来这里的方案加上nums[i]后方案。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; ++i) {
            sum += nums[i];
        }
        if (Math.abs(target) > sum) return 0;
        //奇数没办法做到,比如说sum =5,target=2,你就凑不到。
        if ((sum + target) % 2 == 1) return 0;
        int left = (sum + target) / 2;
        int[] dp = new int[left + 1];
        dp[0] = 1;
        for (int i = 1; i <= nums.length; ++i) {
            for (int j = left; j >= nums[i - 1]; --j) {
                dp[j] += dp[j - nums[i - 1]];
            }
        }
        return dp[left];
    }
}

image-20230802222429647

*一和零

474. 一和零

只能说为数不多dp是自己做出来的题目,其实这道题就是多了一个维度,就会感觉到很复杂,但是其实把题目简化一下,这道题就不会那么难想了,那么就是把题目改了最多有m个0,减少一层维度,你就会发现,这道题,其实就是一个典型的01背包,这么多个字符串,要怎么装进背包,使得0最多,哈哈是不是题目一下子就变简单了。

那么,如果是一维的题目,这道题应该怎么写呢?那么dp数组的含义就是最多有m个0的最大的子集长度。那么现在你装进一个新的字符串,你就要去看一下,你剩下的的0(也就是背包剩余容量)能装最长的长度是多少+选中的字符串(1)与当前不放这个字符串的最长长度的比较。

dp[i] = Math.max(dp[i], dp[i - zeroNum] + 1);

是不是就是清晰了许多,所以现在只是把背包换成了二维的,也就是装i个0,装j个1最长的子集,也就是把两个for变为了三个for,其中两个for是用来遍历二维背包的。

class Solution {
    public class Pair {
        public int one;
        public int zero;
    }
    public int findMaxForm(String[] strs, int m, int n) {
        Pair[] pairArray = new Pair[strs.length];
        //我这里先提前知道字符串的0和1的个数
        for (int i = 0; i < strs.length; ++i) {
            pairArray[i] = get10(strs[i]);
        }
        //二维背包
        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= strs.length; ++i) {
            for (int j = m; j >= pairArray[i - 1].zero; --j) {
                for (int k = n; k >= pairArray[i - 1].one; --k) {
                    //要不要装?
                    //如果装了,那么你就是加入了这个字符串后,剩下的0和1的数量可以装的最长长度加上本身一个字符串比不装的长
                    dp[j][k] = Math.max(dp[j - pairArray[i - 1].zero][k - pairArray[i - 1].one] + 1, dp[j][k]);
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[m][n];
    }

    private Pair get10(String str) {
        Pair pair = new Pair();
        for (char c: str.toCharArray()) {
            if (c == '0') {
                ++pair.zero;
            } else if (c == '1') {
                ++pair.one;
            }
        }
        return pair;
    }
}

image-20230803001510750

看了题解后,没想到第一层for循环,就可以确定0和1的数量了,这样就少了一次额外的for循环。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        //二维背包
        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= strs.length; ++i) {
            int zeroNum = 0;
            int oneNum = 0;
            for (char c: strs[i - 1].toCharArray()) {
                if (c == '0') {
                    ++zeroNum;
                } else {
                    ++oneNum;
                }
            }   
            for (int j = m; j >= zeroNum; --j) {
                for (int k = n; k >= oneNum; --k) {
                    //要不要装?
                    //如果装了,那么你就是加入了这个字符串后,剩下的0和1的数量可以装的最长长度加上本身一个字符串比不装的长
                    dp[j][k] = Math.max(dp[j - zeroNum][k - oneNum] + 1, dp[j][k]);
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[m][n];
    }
}

image-20230803001924454

完全背包

完全背包和01背包不同的点就是01背包就是物品只有一个,而完全背包就会有无限个,还有没有印象,一维dp数组的倒序就是为了解决物品重复放置的问题,所以,其实只要把一维dp数组的倒序变为正序就可以了。

        int[] dp = new int[bagsize + 1];
        for (int i = 0; i < nums.length; ++i) {
            for (int j = nums[i]; j <= bagsize; ++j) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + value[i]);
            }
        }

如果是传统的完全背包,那么物品先遍历还是先背包先遍历是任意的,观察递归公式,他取决的是前面的dp数组的值,而因为第二个for循环是正序的,所以不管是行遍历,还是竖遍历,都会把dp数组前面的值填进去。

零钱兑换II

518. 零钱兑换 II

看到硬币的数量是无限的,然后要使得amout凑得齐,就是一个完全背包,然后求的是填满背包的方式有多少种。

首先明确dp数组的含义,因为是凑满背包的方案数,所以dp数组的含义就是填满j的背包有dp[j]种方案。

对于初始化,装满0块钱的背包的是有1种方法,这样后面计数的时候才不会都是0。

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        //注意初始化的含义,而且有一个样例也是,当amount为0的时候,值为1
        dp[0] = 1;
        for (int i = 0; i < coins.length; ++i) {
            //正序
            for (int j = coins[i]; j <= amount; ++j) {
                //需要是+=
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

image-20230804222150083

组合总和 Ⅳ

377. 组合总和 Ⅳ

完全背包中for循环的顺序有两种情况,一种的排列,一种是组合,就上一题而已,就是组合,而这道题,是排列,那么就要重新考虑一下两个for循环的排序顺序带来的差异了。

如果先遍历物品,再遍历背包,那么当你第一个物品遍历完所有的背包后,然后再去添加第二个,第三个物品,这样也就是只是一个组合了,这样只会有{1,2,3}这种情况。

而先遍历背包,再遍历物品,那么当第一个背包的时候,就会添加物品123,然后到第二个背包,又会尝试添加物品123,所以这样添加起来,就会出现{1,2,3},又会出现{3,2,1}的结果。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        //注意先背包,再物品
        for (int i = 0; i <= target; ++i) {
            for (int j = 0; j < nums.length; ++j) {
                if (i >= nums[j])
                    dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
}

image-20230804235157630

爬楼梯

70. 爬楼梯

爬楼梯有很多种解法,这里选择用背包来解。

这道题,就是和上面那道题完全一模一样,都不用改代码那种。首先明确,这是完全背包,然后1和2是随机组合,所以是排列问题,然后这是求方法数,那么直接填代码就行了。

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

image-20230805000600909

*零钱兑换

322. 零钱兑换

这道题居然一次性写过去了,我当时是震惊的,没想到居然模拟出来了。

一开始,你要知道他是要考的什么题型,一看就是完全背包吧,然后是装满背包吧,然后额外就是最少的硬币,

所以dp数组的含义含义就是装满该背包容量的最少的硬币,因为是最少,所以我们的初始化就不能为0了,我们需要对其进行初始化为最大值,表示无法装满,而对于容量为0的,就是0种方法装满,这个初始化可以自己模拟一下,或者直接看题目的示例,里面也是说的。

遍历顺序,就正常的顺序而已。

主要是递推公式,这里麻烦了一点,因为是需要装满的,所以如果装下当前的,剩余容量无法装满,那就不能继续装,那就不用管就好了。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        // 装满0背包的方法有0种。
        dp[0] = 0;
        //因为是求最小的,所以都需要初始化为最大的。
        for (int i = 1; i <= amount; ++i) {
            dp[i] = Integer.MAX_VALUE;
        }
        for (int i = 0; i < coins.length; ++i){
            for (int j = coins[i]; j <= amount; ++j) {
                //表示装了当前的,剩余的容量无法装满。
                if (dp[j - coins[i]] == Integer.MAX_VALUE) {
                    continue;
                }
                // 装下了当前的,还有剩余容量可以装满,但是值不值得装
                dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

image-20230805003904783

完全平方数

279. 完全平方数

这道题,基本就是和上面那道题一模一样,完全背包,装满背包,方式最少有多少种。

dp数组的含义就是,装满j的背包,最少有多少种方法,也就是平方数相加的和==target的最少的数量。

dp数组初始化,因为是求最小的,所以初始化我们需要求最大的,防止后续dp状态转移值被覆盖,然后对于0,我们还是初始化为0,装满0有0种办法。

dp的顺序也是一样的,是组合,所以先物品在背包,中间是正序。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 0;
        for (int i = 1; i < dp.length; ++i) {
            dp[i] = Integer.MAX_VALUE;
        }
        int num = (int) Math.sqrt(n);
        for (int i = 1; i <= num; ++i) {
           //背包是否放得下?
            for (int j = i * i; j <= n; ++j) {
                //背包剩余的容量是否能被装满?
                if (dp[j - i * i] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j - i * i] + 1, dp[j]);
                }
            }
        }
        return dp[n] == Integer.MAX_VALUE ? 0 : dp[n];
    }
}

image-20230805195734882

单词拆分

139. 单词拆分

对于求出所有排列的情况,对于使用dp没有太多的思路,这里尝试使用回溯爆搜一下,但是大概率是寄的。

class Solution {

    List<String> res = new ArrayList<>();
    public boolean wordBreak(String s, List<String> wordDict) {
        wordBreak(0, wordDict, "", s);
        return res.contains(s);
    }

    private void wordBreak(int startIndex, List<String> wordDict, String path, String s) {
        System.out.println(path);
        if (path.length() == s.length()) {
            res.add(path);
            return;
        }

        for (int i = 0; i < wordDict.size(); ++i) {
            wordBreak(i + 1, wordDict, path + wordDict.get(i), s);
        }
    }
}

image-20230805201449953

果然还是不行,还是得好好想一下dp应该怎么做。

其实好好想过之后,就会发现,这道题还是一个完全背包,就是一个字符串, 你需要丢一些字符串进去,让他装满,核心就是在于,如何判断装满,这是这道题比较难思考的一个点。

我们把s一个字符串,例如leetcode划为多个背包的不同容量,l le lee leet,这样就是一个个不同背包的容量了,然后我们只需要根据背包的容量,截取一下相应物品大小的字符串,如果是一样的字符串,就表明可以填进去,但是填进去后呢,剩余背包(剩下的字符串)是否被装满,就需要往前看。

另外的一个关键是,这是一个排列,所以我们要先背包,再物品。

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;

        for (int j = 0; j <= s.length(); ++j) {
            for (int i = 0; i < wordDict.size(); ++i) {
                //背包容量够装?
                if (j >= wordDict.get(i).length() 
                    //剩余的容量装得满?
                    && dp[j - wordDict.get(i).length()] == true 
                    //截取的字符串是一样的?
                    && s.substring(j - wordDict.get(i).length(), j).equals(wordDict.get(i))) {
                        dp[j] = true;
                    }
            }
        }
        return dp[s.length()];
    }
}

image-20230805220003534

多重背包

多重背包其实就是在01背包上,变成了物品有一定的数量,那其实要解决掉多重背包很简单,只需要把多重背包转换为01背包即可,也就是重构物品集合,把物品全部平铺出来,变为物品重复,但是数量都是为1,解决即可。

打家劫舍

198. 打家劫舍

这种题是新遇到的,但是我发现规律其实很简单,一般我们dp数组,都是用来存之前走过的最大值,所以直接比较dp[i-1],但是这次不行,需要跳过一个有监控的,但是你跳过了监控,不代表上上个监控不能可以有最大值,所以你需要取倒数第二个和倒数第三个的最大值即可,

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

当你遍历到9的时候,除了8不能抢,其实前面的都能抢,也就是奇数列都能抢,偶数列除了隔壁不能抢,而dp数组是存了之前的最大值,所以你只要看一下,你需要抢第7家还是第6家就行了。

class Solution {
    public int rob(int[] nums) {
        int[] dp = new int[nums.length + 3];
        for (int i = 0; i < nums.length; ++i) {
            dp[i + 3] = Math.max(dp[i + 3 - 2], dp[i]) + nums[i];
        }
        return Math.max(dp[nums.length + 2], dp[nums.length + 1]);
    }
}

image-20230805225843381

但是,对于该dp数组,还可以进行优化,因为你看到,只需要利用到前面三个变量,前一个,前两个,前三个,所以直接利用三个变量改造一下即可。

class Solution {
    public int rob(int[] nums) {
        int pre = 0;
        int prepre = 0;
        int preprepre = 0;
        for (int i = 0; i < nums.length; ++i) {
            int cur = Math.max(prepre, preprepre) + nums[i];
            preprepre = prepre;
            prepre = pre;
            pre = cur;
        }
        return Math.max(pre, prepre);
    }
}

image-20230805230933654

看到题解,麻了,居然还有只用两个的,但是细看,感觉还真的是只有两个变量在进行操作。

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) return nums[0];
        if (nums.length == 2) return Math.max(nums[0], nums[1]);
        int pre = Math.max(nums[0], nums[1]);
        int prepre = nums[0];
        for (int i = 2; i < nums.length; ++i) {
            // 如果隔了一个抢之后,还没有隔壁的钱多,不如我等到下一个房间,我再来抢。
            int cur = Math.max(pre, prepre + nums[i]);
            prepre = pre;
            pre = cur;
        }
        return pre;
    }
}

image-20230806000810749

213. 打家劫舍 II

主要的难点在于,他变成了一个环,所以我们现在需要考虑一下首尾的问题,就是说首尾取还是不取,取一还是取0?

其实我们可以转换一下思路,如果我直接把数组切分成两个,一个是只包含头的,一个是只包含尾的,然后分别计算一下他们的最大抢夺,然后两个再比较一下不就可以了吗?即使我们的数组有含头,但是也不一定会有头,但是一定不会有尾巴,所以不会被报警,尾巴的也是如此。

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) return nums[0];
        if (nums.length == 2) return Math.max(nums[0], nums[1]);
        int r1 = rob(nums, 0, nums.length - 1);
        int r2 = rob(nums, 1, nums.length);
        return Math.max(r1, r2);
    }

    private int rob(int[] nums, int startIndex, int end) {
        if (nums.length == 3) return Math.max(nums[startIndex], nums[end - 1]);
        int p = Math.max(nums[startIndex + 1], nums[startIndex]);
        int pp = nums[startIndex];
        for (int i = startIndex + 2; i < end; ++i) {
            int cur = Math.max(pp + nums[i], p);
            pp = p;
            p = cur;
        }
        return p;
    }
}

image-20230806215853094

337. 打家劫舍 III

对于这种题,称为树形dp,对于第一次遇到这种题的,还真的是无从下手,但是,我们有之前做过一道贪心的监控二叉树的题,发现状态转移都是靠后序遍历的返回值提供,这里我们看看能不能也是以一样的思路。

因为打家劫舍,归咎到底就是偷或者不偷的问题,也就是偷了后,左右两个节点都不能偷的总价值,和不偷,左右结点任意的总价值的比较。所以,对于传递返回值,我们需要传递偷的最大值,和不偷的最大值,然后给上一层去讨论偷还是不偷。

class Solution {
    public int rob(TreeNode root) {
        int[] res = robTree(root);
        return Math.max(res[0], res[1]);
    }
    private int[] robTree(TreeNode root) {
        if (root == null) return new int[]{0, 0};
        int[] left = robTree(root.left);
        int[] right = robTree(root.right);

        //如果不偷,那么孩子结点的状态都不会有任何影响,那么组合一个最大的价值即可
        int leftMax = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        //如果偷的话,那么就左右孩子都不能偷,找到不偷的最大价值和当前结点的价值即可
        int rightMax = left[0] + right[0] + root.val;
        return new int[]{leftMax, rightMax};
    }
}

image-20230806230105361

买卖股票

121. 买卖股票的最佳时机

贪心

在遍历中,找到最小的值,也就是买入点,然后一直更新能卖出的最大的钱即可。思路其实很直接,就是找到最便宜的,和最贵的

class Solution {
    public int maxProfit(int[] prices) {
        int min = Integer.MAX_VALUE;
        int res = 0;
        for (int i = 0; i < prices.length; ++i) {
            min = Math.min(prices[i], min);
            res = Math.max(prices[i] - min, res);
        }
        return res;
    }
}

image-20230807214113314

动态规划

对于股票,有两种状态,一种是持有股票,另一种是没有持有股票,持有股票可能是之前一直有,也有可能是当前买了;未持有,可能是之前一直没有,也有可能是当前卖了。

所以dp数组的含义就是两种状态下,赚的最多得钱。

递推公式:

  1. 持有,是不是就是要看在没有股票的情况下要不要换一张买入,结果就是买一张最便宜的。
  2. 未持有,要不要卖呢?卖是建立在之前有股票的情况下,如果卖了后能回本,并且还比我之前卖的股票还赚,当然要卖了
class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[2][prices.length];
        // 0是未持有股票,1是持有股票
        //未持有,可能是之前一直没有,也有可能是当前卖了
        // 持有,可能是之前一直有,也有可能是当前买了。
        dp[1][0] = -prices[0];
        dp[0][0] = 0;

        for (int i = 1; i < prices.length; ++i) {
            // 是否值得买入,或者继续维持之前的
            dp[1][i] = Math.max(dp[1][i - 1], -prices[i]);
            // 是否值得卖出,或者继续维持之前的
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] + prices[i]);
        }
        return dp[0][prices.length - 1];
    }
}

image-20230807215833483

122. 买卖股票的最佳时机 II

这道题,就和上面的有一点不太一样了,他是说能买卖多次。

贪心

因为这个是可以买卖多次,所以如果我只要盈利了,我就可以直接买卖,然后我一直累加盈利就行了,所以只需要求出左右的差值是否为正的,那么全部累加就行了。

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 1) return 0;
        int sum = 0;
        for (int i = 1; i < prices.length; ++i) {
            if (prices[i] - prices[i - 1] > 0) {
                sum += prices[i] - prices[i - 1];
            }
        }
        return sum;
    }
}

image-20230807220111490

动态规划

和上面的题的动态规划思路基本一样,最大的不同就是他能买卖多次股票,所以他就需要考虑一下之前卖出股票后剩下的钱。

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[2][prices.length];
        dp[1][0] = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            // 这里不同于上一题,因为上一题只有一次买卖,他就不需要考虑到当前卖股票后剩下的钱。
            //其实上一题可以写为0 - prices[i]
            dp[1][i] = Math.max(dp[1][i - 1], dp[0][i - 1] - prices[i]);
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] + prices[i]);
        }
        return dp[0][prices.length - 1];
    }
}

image-20230807221121316

123. 买卖股票的最佳时机 III

当你会了前面两道题了后,这道题差不多就能照葫芦路画瓢写个大概出来,前面都是买一次,或者尽量买卖的最大价值,所以也就是只有两个状态,持有股票,未持有股票,但是这道题就复杂了一点,多了最多两次买卖,那么也就是第一次持有,第一次未持有,第二次持有,第二次未持有。

对于第一次持有和第一次未持有,代码是和第一题的买卖股票一样的,因为只能买卖一次,而对于第二次持有,是不是只能从之前的状态推过来的,或者由第一次未持有推过来的,对于第二次未持有,是不是只能从之前推过来的,或者由第二次持有过来的。

所以我们定义0为第一次未持有,1为第一次持有,2为第二次未持有,3为第二次持有。

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[4][prices.length];
        dp[1][0] = -prices[0];
        dp[3][0] = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] + prices[i]);
            dp[1][i] = Math.max(dp[1][i - 1], 0 - prices[i]);
            dp[2][i] = Math.max(dp[2][i - 1], dp[3][i - 1] + prices[i]);
            dp[3][i] = Math.max(dp[3][i - 1], dp[0][i - 1] - prices[i]);
        }
        return dp[2][prices.length - 1];
    }
}

image-20230807225525169

188. 买卖股票的最佳时机 IV

只能说,这道题就是在上面那道题简单地改了一下,买卖一次,两个状态,买卖两次,4个状态,那k次,不就是2 * k个状态了吗,然后接着只需要列出规律即可。

class Solution {
    public int maxProfit(int k, int[] prices) {
        int[][]dp = new int[2 * k + 1][prices.length];

        for (int i = 1; i < 2 * k; i += 2) {
            dp[i][0] = -prices[0];
        }

        for (int i = 1; i < prices.length; ++i) {
            for (int j = 1; j < 2 * k + 1; ++j) {
                if (j % 2 == 1) {
                    dp[j][i] = Math.max(dp[j][i - 1], dp[j - 1][i - 1] - prices[i]);
                } else {
                    dp[j][i] = Math.max(dp[j][i - 1], dp[j - 1][i - 1] + prices[i]);
                }
            }
        }
        return dp[2 * k][prices.length - 1];
    }
}

image-20230807231836321

309. 买卖股票的最佳时机含冷冻期

这道题的状态设置就有点技巧了,应该设置为持有、卖出之后持有的、卖出,那么这么设置,持有就依赖之前的或者卖出之后持有的,卖出之后持有的就依赖卖出或者之前的,卖出就依赖之前的或者持有的。

所以买卖股票这类问题主要是状态设置和依赖的关系,如果一开始的找状态非常难的时候,你就要想一下,这个状态依赖于什么呢?

比如这道题,持有股票,那么就是有或者买入,如果买入,我应该依赖什么状态呢?是不是就是冷冻期之后的所有时间,而冷冻期的这个状态,是由什么决定呢?是不是依赖于卖出的那个状态,而卖出状态呢?就是依赖持有的状态,最后形成闭环,题目也就解决了。

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[3][prices.length];

        dp[0][0] = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] - prices[i]);
            dp[1][i] = Math.max(dp[1][i - 1], dp[2][i - 1]);
            dp[2][i] = Math.max(dp[2][i - 1], dp[0][i - 1] + prices[i]);
        }

        // System.out.println(Arrays.deepToString(dp));
        return dp[2][prices.length - 1];
    }
}

image-20230808003112906

714. 买卖股票的最佳时机含手续费

这道题基本就和上面那道题一样喽,上面那题是在卖出后增加冷冻期,跳过一天,这道题是卖出后增加手续费,然后不需要跳过一天,所以依赖关系就有点不一样了,卖出后保持的状态,是需要依赖于今天的卖出状态的。

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[3][prices.length];
        dp[0][0] = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] - prices[i]);
            //需要先计算今天卖不卖
            dp[2][i] = Math.max(dp[2][i - 1], dp[0][i - 1] + prices[i] - fee);
            //然后再计算卖之后保持的状态
            dp[1][i] = Math.max(dp[1][i - 1], dp[2][i]);
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[2][prices.length - 1];
    }
}

image-20230808004556863

但是其实你会发现,这个状态虽然写出来之后,好像是可以合并的,因为计算今天卖不卖的同时,dp[1]的计算又是重复了的。

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[2][prices.length];
        dp[0][0] = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            dp[0][i] = Math.max(dp[0][i - 1], dp[1][i - 1] - prices[i]);
            //需要计算今天卖不卖
            dp[1][i] = Math.max(dp[1][i - 1], dp[0][i - 1] + prices[i] - fee);
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[1][prices.length - 1];
    }
}

image-20230808004707652

*最长递增子序列

300. 最长递增子序列

对于动态规划,我们还是遵循动规五部曲

首先我们先确定dp数组的含义,题目要求最长的递增子序列,所以dp[i]就是在0到i的范围内,最长的递增子序列的长度。

接着我们需要确定递推公式,因为dp数组的含义是范围内的最长子序列,所以我们要取哪一个数,还需要进行遍历找,然后找到一个值得作为之前的序列为止。

遍历顺序只需常规遍历,然后在选择的时候再来一个for,遍历顺序任意。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length + 1];
        //初始化
        dp[0] = 1;
        int max = 1;
        for (int i = 1; i < nums.length; ++i) {
            //初始化
            dp[i] = 1;
            //需要找一个最长的,并且比他小的,当前面的序列
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) 
                    //是否选了后会变得更长
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
            }
            //需要找到所以结尾的情况的最长子序列
            max = Math.max(dp[i], max);
        }
        return max;
    }
}

image-20230810151258633

最长连续递增序列

674. 最长连续递增序列

如果懂了上面那道题,这道题其实也就非常轻松了,不懂都可以照葫芦画瓢写出来。这道题最主要的区别就是他是连续的,所以就不需要考虑前面的是否值得装,如果比之前的大,一定会比前面的长。

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int max = 1;
        for (int i = 1; i < nums.length; ++i) {
            dp[i] = 1;
            if (nums[i] > nums[i - 1]) {
                dp[i] = Math.max(dp[i - 1] + 1, dp[i]);
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

但是其实,这个dp[i]需要取大的吗,其实不用的,因为上面分析,是连续的情况。

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int max = 1;
        for (int i = 1; i < nums.length; ++i) {
            dp[i] = 1;
            if (nums[i] > nums[i - 1]) {
                // dp[i] = Math.max(dp[i - 1] + 1, dp[i]);
                dp[i] = dp[i - 1] + 1;
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

image-20230810155226532

暴力解法也很简单

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int res = 1;
        int cnt = 1;
        for (int end = 1; end < nums.length; ++end) {
            if (nums[end] > nums[end - 1]) {
                ++cnt;
            } else {
                cnt = 1;
            }
            res = Math.max(cnt, res);
        }
        return res;
    }
}

image-20230810161848228

*最长重复子数组

718. 最长重复子数组

这道题的核心就是,想到dp数组的含义是什么,而且要知道是利用二维dp数组写的。

首先,二维dp数组dp[i][j]的含义是nums1,中i-1前的数据和nums2中j-1前的数据最长的重复子数组。

接着,因为重复子数组的长度,我们需要根据前面的数组的值进行推导,所以递推公式为:dp[i][j] = dp[i - 1][j - 1] + 1

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 2][nums2.length + 2];
        int max = 0;
        for (int i = 1; i <= nums1.length; ++i) {
            for (int j = 1; j <= nums2.length; ++j) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    max = Math.max(max, dp[i][j]);
                }
            }
        }
        return max;
    }
}

image-20230810221417852

然后通过背包那边的,我们知道这条递推公式其实可以利用滚动数组进行压缩。

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[] dp = new int[nums2.length + 2];
        int max = 0;
        for (int i = 1; i <= nums1.length; ++i) {
            for (int j = nums2.length; j >= 1; --j) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[j] = dp[j - 1] + 1;
                    max = Math.max(max, dp[j]);
                } 
                //注意这里的赋值0,避免上一层对此层进行干扰
                else {
                    dp[j] = 0;
                }
            }
        }
        // System.out.println(Arrays.toString(dp));
        return max;
    }
}

image-20230810225404193

*最长公共子序列

1143. 最长公共子序列

会了上面那道题,其实这道题还是差不多的,只不过因为这是可以不需要要求连续,所以有点坑。

对于dp数组的定义,就和上道题一样。

但是递推公式,如果相等的话,也是一样的,通过两个数组之前的进行推导;但是不相等的情况下呢?因为上面一题是要求连续的,所以一旦不一样,就需要设置为0,但是这题一样,如果不一样,但是还可以通过前面的推导出来,前面的推导有两种,这个很核心,一种是第一个数组-1,另一种是第二个数组-1。

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[len1][len2];
    }
}、

image-20230815001251151

不相交的线

1035. 不相交的线

其实,这道题,你只要知道了规律,那么直接就秒杀,不相交的线,然后还要找到一样的,一样的,不就是重复的子序列吗,然后不相交,其实就是顺序一样的,那么这道题,就是变成了最长的公共子序列的问题了,和上面的代码完全一模一样。

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[len1][len2];
    }
}

image-20230815000847410

最大子序和

53. 最大子数组和

这道题的递推公式还是比较好想的,因为是连续的,所以是依赖于前一个元素。

class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length + 1];
        dp[0] = nums[0];
        int max = nums[0];
        for (int i = 1; i < nums.length; ++i) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            max = Math.max(dp[i], max);
        }
        return max;
    }
}

image-20230812000556717

可以发现,因为只是依赖了前一个元素,所以可以直接用一个变量代替即可。

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = nums[0];
        int max = nums[0];
        for (int i = 1; i < nums.length; ++i) {
            pre = Math.max(pre + nums[i], nums[i]);
            max = Math.max(pre, max);
        }
        return max;
    }
}

image-20230812000717796

判断子序列

392. 判断子序列

其实这道题是和最长公共子序列是一样的题目,只不过那道题是互相成为子序列,而这道题是有指定是哪一个字符串是子序列的,所以递推公式就需要作出一点改变。

class Solution {
    public boolean isSubsequence(String s, String t) {
        if (s.length() > t.length()) return false;
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        int max = 0;
        for (int i = 1; i <= s.length(); ++i) {
            for (int j = 1; j <= t.length(); ++j) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } 
                //这里是只会判断长的序列的情况,而不是根据短序列的情况。
                else {
                    dp[i][j] = dp[i][j - 1];
                }
                max = Math.max(dp[i][j], max);
            }
        }
        return max == s.length();
    }
}

image-20230814235136903

**不同的子序列

115. 不同的子序列

这道题关键是递推公式的推导,对于两个值相等,可以从两个序列的前面推导出来,但是还可以怎么推导呢?其实可以t不变,从s-1的地方进行推导,比如说rabb 和rab,可以从rab ra推导,也可以从rab rab推导出来对吧,这样就有两种结果了。但是对于两个值不相等呢?其实就是可以从前面s-1进行推导,因为我们只能删除s,而不能删除t。

对于初始化,也是有难度的,我们可以想一下,如果t的长度是0,那么直接初始化为1就行了,那么也就是把第一列都初始化为1。

class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 0; i < s.length(); ++i) {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= s.length(); ++i) {
            for (int j = 1; j <= t.length(); ++j) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[s.length()][t.length()];
    }
}

image-20230812202625448

两个字符串的删除操作

583. 两个字符串的删除操作

这道题,其实和不相交的线的题目同一样的,就是在经典动态规划的题目上加以修改,然后变成了一个新的题目。

你可以看一下,删除字符,然后使得两个字符串最后一样,一样是什么意思,公共部分,而且顺序一样,就是子序列,其实这道题就是求的是两个字符串的最长的公共子序列。

最后要删除的个数,其实就是最长公共序列的长度,和两个原始长度的差值了。

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; ++i ) {
            for (int j = 1; j <= len2; ++j) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return len1 - dp[len1][len2] + len2 - dp[len1][len2];
    }
}

image-20230814224707587

这道题还有另外一种解法,就是按照题目来进行一个dp,然后有了这个做法之后,下面的编辑距离就会有一点思路了。

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        //注意这里初始化,因为要删除掉字符串的所有长度
        for (int i = 0; i <= len1; ++i) dp[i][0] = i;
        for (int j = 0; j <= len2; ++j) dp[0][j] = j;
        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    //看要删哪个字符串
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
                }
            }
        }
        return dp[len1][len2];
    }
}

image-20230814224344438

*编辑距离

72. 编辑距离

这道题和上一道题有点类似,但是这里的操作数变成了3种,插入删除修改,所以在不相等的时候,我们要去判断每一种操作最少的操作数。然后这道题还有一个神仙的思路,就是增加操作,其实是等同于另一个字符串删除操作的,这一步很关键。

当我们删除的时候,一个字符串的长度不会变,另一个变化,所以我们需要判读的是s-1和j的dp值。而修改的话,可以认为是已经是相等的,只不过是加了一个操作数而已。

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        //注意这里的初始化,当一个字符串为0的时候,我们需要增加,或者删除另一个, 注意是<=
        for (int i = 0; i <= len1; ++i) {
            dp[i][0] = i;
        }
        for (int i = 0; i <= len2; ++i) {
            dp[0][i] = i;
        }

        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    //判断三种操作的dp值
                    //增加等同于另一个字符串删除
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i - 1][j - 1]), dp[i][j - 1]) + 1;
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[len1][len2];
    }
}

image-20230812222854665

回文子串

回文子串

这道题的dp数组的定义,再也不是我们想的那样了,就是直接题目求什么,dp数组就是求什么了,这道题,你要先想一想,如何才能有递推的规律。

判断回文子串,在判断字符串的两头的时候,是不是当两头相等,只要是中间也是回文串的话,那么这个也是回文串,那么这样就能推出一个递推的关系了,而为了表示i和j,那么就需要二维dp的数组,去表示i到j的范围内,是否是回文字符串。

那么现在我们就来继续完善一下递推的公式,字符串两头,如果是只有一个字符呢?当然是回文,如果是两个且相等呢?那么也是,如果大于2个的字符串呢?两头相等,那么就是dp[i + 1][j - 1]

现在再来考虑一下遍历的顺序,可以看到,递推的公式其中的i+1,是需要依赖下面的行的,所以i的一层遍历一定是倒序的,而j - 1就依赖前面的,直接正序就行了。

class Solution {
    public int countSubstrings(String s) {
        boolean[][] dp = new boolean[s.length()][s.length()];
        int cnt = 0;
        for (int i = s.length() - 1; i >= 0; --i) {
            for (int j = i; j < s.length(); ++j) {
                if (s.charAt(i) == s.charAt(j)) {
                    if (i == j || i + 1 == j || dp[i + 1][j - 1]) {
                        dp[i][j] = true;
                        ++cnt;
                    }
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return cnt;
    }
}

image-20230814222052901

最长回文子序列

516. 最长回文子序列

这道题和上面的一道题有点相似,核心就是一个递推公式怎么推导的问题,这道题就和上面的不一样了,这是一个不需要连续的,所以是有一个叫做删除字符串的操作,所以是可以删两个字符串的,所以是会有两种情况。然后推导的时候,如果中间字符串的回文序列的长度是4的话,其实就是4+两头=6,但是i==j的话,就应该是1了,然后i+1==j就应该算2。

class Solution {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len + 2][len + 2];
        for (int i = len; i >= 1; --i) {
            for (int j = i; j <= len; ++j) {
                if (s.charAt(i - 1) == s.charAt(j - 1)) {
                    if (i == j) {
                        dp[i][j] = 1;
                    } else if (i == j - 1){
                        dp[i][j] = 2;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                    }
                } else {
                    //注意这里,删除字符串,可以删左头,也可以删右头。
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[1][len];
    }
}

image-20230814221639414

评论

  1. 博主
    1 年前
    2023-5-22 0:21:53

    欢迎大家批评指正!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇