代码随想录二刷笔记

449 阅读32分钟

1. 数组

1.1 704 二分查找 💚

二分查找使用前提:1. 有序数组 2. 数组中无重复元素

要搞清楚边界条件,使用左闭右闭区间时,循环条件是 left <= right。因为例如[2,2]这个边界条件,middle可以取值为2,有意义。且每次条件判断完后,left和right都要middle +- 1。因为边界条件已经不符合了。

1.1.1 35 搜索插入位置 💚

这题和上题搜索位置的思路是一样的,但要注意插入位置的4种情况

  1. 目标值在数组所有元素之前 [0, -1]
  2. 目标值等于数组中某一个元素 return middle;
  3. 目标值插入数组中的位置 [left, right],return right + 1
  4. 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1.

所以在循环中,如果找到就return mid,找不到循环结束后return right + 1

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

这个题难点在于怎么找到左右边界,以及左右边界的判断条件。

  1. target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
  2. target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
  3. target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}

其次就是如何找到左右边界,找两个边界的时候,都是寻找target的左右相邻的那个位置,而非target本身。

  • 对于最终左边界,要找到target中最左边的值的左边一个元素,所以while中的条件判断就是if(nums[mid] >= target),这样,即使找到了target,也会继续判断是不是最左边的target,此时更新右边界的同时,更新最终左边界。
  • 对于最终右边界,要找到target中最右边的值的右边一个元素,所以while中的条件判断就是if(nums[mid] <= target),这样,即使找到了target,也会继续判断是不是最右边的target,此时更新左边界的同时,更新最终右边界。

1.1.3 69. x的平方根 💚

范围就是[0,x], 初始条件left = 1, right = x。进行二分查找,要使用if(mid > x/mid) 判断,因为如果mid相乘,可能会越界,所以用除法判断。其次,最终返回右边界,所以大于时,变左边界,小于等于时变右边界。这样可以确保的是,只要能进循环,且mid*mid>x时,右边界可以-1。特殊情况,x是0时,不进循环,直接返回右边界就是0,所以不需要特别注意0.

1.1.4 367 完全平方数 💚

思路于与上一题一模一样,最后判断right的平方是不是num。

可以直接在while循环中判断,这样更容易理解。不需要返回边界。

1.2 双指针

1.2.1 27 移除元素 💚

快慢指针。快指针指到不是要删除的元素时,给慢指针

1.2.2 26 删除排序数组中的重复项 💚

快慢指针,但在覆盖条件判断时,有些巧妙,要把快慢指针都设置成1,然后判断if(nums[slow - 1] != slow[fast]),之后覆盖。

1.2.3 283 移动零 💚

快慢指针。快指针不是0是覆盖慢指针,循环结束后在数组最后加0。

1.2.4 844 比较含退格的字符串 💚⛽️

快慢指针。并不能理解

也可以用stack做,不是#就push,是#就pop,最后比较两个stack。注意stack为空时,不用pop,直接continue。还有最后两个stack比较时,如果比较完成后,还需要判断两个stack是不是都空了。

1.2.5 977 有序数组的平方 💚⛽️

双指针,一个在开头,一个在结尾。那个平方大,就把那个放进答案里。放进答案的时候,很重要的一点是从后往前放,因为整个数组是non-decreasing的,所以要两个指针的平方要比较最大的放在最后,这样才能保证最后result也是non-decreasing的。

1.4 滑动窗口

1.4.1 209 长度最小子数组 🧡⛽️

滑动窗口,for循环中滑动的是end位置,在for循环中的判断,判断的是start位置。对于每一次循环,计算sum,当sum大于等于target的时候,移动start位置。这里用while循环,因为可以一直前推start的位置,直到找到最小的。在while循环中,判断子数组长度以及更新res值,之后往后推start的位置。

1.4.2 904 水果成篮🧡/159 最多包含两个不同字符的最长子串🧡⛽️

两个题一模一样。

滑动窗口的套路就是for循环移动end的位置,在循环中使用while移动start的位置。即满足条件时,缩小窗口大小。

本题使用hashmap存放种类以及数量,一开始把种类放入map中,并记录数量,如果数量大于限制(2),则使用while循环移动start位置。在while循环中,首先获取start指针所在种类的数量,如果大于1,就减1,并重新放回map,else直接删除这个元素。每次while结束后,更新res。

一些相关函数: HashMap<Integer, Integer> map = new HashMap<>()/map.containsKey()/map.put()/map.remove()/Math.max()

1.4.3 76 最小覆盖子串❤️

思路:

  1. 统计所需字符及其数量
  2. 使用滑动窗口,当滑动窗口中的字符数量与所需要的字符数量匹配时,缩小窗口。

1.5 螺旋矩阵

1.5.1 59 螺旋矩阵2 🧡⛽️

不是代码随想录的做法。 左闭右闭,上下时+—1。 左右时左闭右闭,正常判断。上下时左闭右闭,起始条件+1,终止条件-1。注意n是基数时,要把中间的数塞进去。判断终止条件:count < n*n。

备份做法:

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res = new int[n][n];
        int count = 1;
        int left = 0;
        int right = n - 1;
        int up = 0;
        int down = n - 1;
        while(count < n * n){
            for(int j = left; j <= right; j++){
                res[up][j] = count++;
            }
            for(int i = up + 1; i <= down - 1;i++){
                res[i][right] = count++;
            }
            for(int j = right; j >= left; j--){
                res[down][j] = count++;
            }
            for(int i = down - 1; i >= up + 1; i--){
                res[i][left] = count++;
            }
            left++;right--;up++;down--;
        }

        if(n % 2 == 1){
            res[n/2][n/2] = count;
        }
        return res;
    }
}

1.5.2 54 螺旋矩阵🧡⛽️

判断边界同上。注意创建arraylist,循环判断也要用arraylist 是不是满了。转圈while的循环条件是res.size() < row * col,循环完成一圈以后,上下左右的边界一起移动。并且圈中的单方向打印,除了限制边界条件,也要限制res有没有满! 这是因为每次进循环的时候,边界条件是一样的,如果不限制res满不满,就会出现从左到右打印之后,从右到左又重复打印。

备份做法:

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        if(matrix == null || matrix.length == 0){
            return res;
        }
        int left = 0;
        int right = matrix[0].length - 1;
        int up = 0;
        int down = matrix.length - 1;
        int row = matrix.length;
        int col = matrix[0].length;
        while(res.size() < row * col){
            for(int j = left; j <= right && res.size() < row * col; j++){
                res.add(matrix[up][j]);
            }
            System.out.println(res);
            for(int i = up + 1; i <= down - 1 && res.size() < row * col; i++){
                res.add(matrix[i][right]);
            }
            System.out.println(res);
            for(int j = right; j >= left && res.size() < row * col; j--){
                res.add(matrix[down][j]);
            }
            System.out.println(res);
            for(int i = down - 1; i >= up + 1 && res.size() < row * col; i--){
                res.add(matrix[i][left]);
            }
            System.out.println(res);
            left++;right--;up++;down--;
        }
        return res;
    }
}

2. 链表

链表循环终止条件🌟

  • node != null : 若指针每次移动一步,则指针最终指向 null ;适用于尾节点需要处理。
  • node.next != null : 若指针每次移动一步,则指针最终指向链表尾节点;适用于尾节点不需要处理,例如 删除链表中某个节点 。
  • node != null & node.next != null : 若每次移动两步,则指针最终指向不确定;例如 适用于链表环问题。

2.1 203 移除链表元素 💚

建立虚拟头节点dummy,把dummy指向head。pre设为dummy,cur设为head。如果当前值是要 删除的,把dummy.next指向cur.next。否则,cur设为pre。判断完成后,cur后移。

2.2 707 设计链表 🧡⛽️

很多细节。

  • 先建另一个class,为ListNode。
  • MyLinkedList中的属性为size和ListNode
  • 对于get方法,先判断index有没有双边越界,再用for loop找到节点,并返回节点的值
  • 对于addAtHead/addAtTail,其实就是特殊位置的额addAtIndex,直接调用函数即可
  • 对于addAtIndex,超最大边界时return,超最小边界时,index设为0。size++。用pre节点记录位置,找到并插入
  • 对于deleteAtIndex,超双边边界return,特别对于删除头节点,index为0的情况,直接返回head.next。size--。用pre节点记录位置,找到并删除。

2.3 206 反转链表 💚

  • 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

  • 然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

  • 为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

  • 接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

  • 最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

2.4 24 两两交换链表中的节点 🧡

改变过程。注意循环条件是cur.next != null && cur.next.next != null 两两交换链表中的节点

2.5 19 删除链表中倒数第N个节点 🧡

  • 首先使用虚拟头结点
  • 定义fast指针和slow指针,初始值为虚拟头结点
  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点
  • fast和slow同时移动,直到fast指向末尾
  • 删除slow指向的下一个节点

2.6 160 链表相交 💚

  • 求两个链表的长度
  • 然后让curA的指针移动到和curB末尾对齐的位置。这一步要保证A是长的那个,如果B比A长,就交换头节点和长度
  • 此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。

2.7 142 环形链表2 🧡

快慢指针一起走,快指针走两步,慢指针走一步。相遇时一个指针回head,一起走一步,相遇的地方就是入环口。注意要判断是不是环,再判断入环口。

3. Hash

3.1 字母哈希

3.1.1 242 有效的字母异位词 💚

因为只有字母,所以可以建立一个长度为26的数组,记录每个字母出现的次数

  1. 记录第一个字符串每个字母出现次数
  2. 用记录中的次数,减去第二个字符串每个字母出现的次数
  3. 看最终的记录是不是每个字母都是0

3.1.2 383 赎金信 💚

方法同上

3.1.3 49 字母异位词分组 🧡

  1. 对于每一个String先转成array
  2. Arrays.sort 按字母顺序排序
  3. String.valueOf()记做key
  4. 寻找map中有没有存在的key
  5. 打印new ArrayList(map.values())

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

滑动窗口

  1. 先记录目标String中字母个数
  2. 使用滑动窗口,加入字母
  3. 一旦窗口超过目标长度,就把最左边的踢出去
  4. 使用Arrays.equals()比较是否相等,想等就把答案加入
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        ArrayList<Integer> res = new ArrayList<>();
        int[] target = new int[26];
        int[] temp = new int[26];
        for(int i = 0; i < p.length(); i++){
            target[p.charAt(i) - 'a']++;
        }
        
        for(int i = 0; i < s.length();i++){
            temp[s.charAt(i) - 'a']++;
            if(i >= p.length()){
                temp[s.charAt(i - p.length()) - 'a']--;
            }
            if(Arrays.equals(temp, target)){
                res.add(i - p.length() + 1);
            }
        }
        return res;
    }
}

3.2 349 两个数组的交集 💚

使用HashSet解题

  1. 用set记录一个数组中的元素
  2. 循环第二个数组,看是否匹配,匹配记入result set
  3. set转换arrayres.stream().mapToInt(x -> x).toArray()

3.3 202 快乐数 💚⛽️

用set记录数字有没有循环过,避免重复判断。

3.4 1 两数之和 💚

要用HashMap,因为不仅要存值,还要把对应的坐标也存下来。

  • 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。

  • 那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。

  • 所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

  • 在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。

3.5 454 四数相加2 🧡

本题比是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!不需要去重。

思路:

  1. 首先定义 一个map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了

3.6 15 三数之和 🧡

这题用hash不好,因为去重麻烦。可以用双指针。

分3个指针,第一个是贯穿数组的指针i,第二个是i右边的指针left,第三个是数组最后位置的指针right。基本思路就是对于每一个i,移动left和right指针,找有没有符合标准的值。

Arrays.sort()排序,然后循环中就可以看移动指针。总数<target,left右移,总数>target,right左移。

另外,去重也很重要,对于a,i > 0 && nums[i] == nums[i - 1],比较于前面的元素是否一样去重。因为不能有重复的三元组,但三元组内的元素是可以重复的。如果于后面的元素比较去重,就是去除三元组内的重复元素。对于b,c去重,就是比较下一个位置的元素是否相同,相同就跳过。

Time: O(n^2)

3.7 18 四数之和 🧡

思路同上,再加一层for loop。 Time O(n^3)

4. String

4.1 344 反转字符串 💚

前后双指针,两两交换,指针向中间移动

4.2 541 反转字符串2 💚

每2k次交换,这样 step size of for loop 就可以设置成2*k。其次,要判断尾数够不够k个来取决end指针的位置,int end = Math.min(ch.length - 1,start + k - 1)。这样交换就可以了前后双指针的字符就可以了,就跟上题一样了。

4.3 剑指05 替换空格 🧡

双指针。注意要从后往前填充。因为从前往后的时候,如果改变前面的,后面的也要向后平移,从前往后就可以避免。

  1. 循环一遍字符串,有空格就扩充string,StringBuilder str = new StringBuilder();
  2. 双指针,一个指向原来的结尾,另一个指向新的结尾。一起向前移动
  3. 如果不是空格,就复制。如果是空格。后面的指针输入0,2,%。
  4. return new String(chars);

4.4 151 翻转字符串里的单词 🧡

三步操作

  1. 移除多余空格
  2. 将整个字符串反转
  3. 将每个单词反转

举个例子,源字符串为:"the sky is blue "

  1. 移除多余空格 : "the sky is blue"
  2. 字符串反转:"eulb si yks eht"
  3. 单词反转:"blue is sky the"

使用StringBuilder 直接改变String

  1. 移除多余空格: 先移除首尾空格,然后对于剩余部分,判断s.charAt(left) != ' ' || sb.charAt(sb.length() - 1) != ' '之后,sb.append()
  2. 字符串反转。其中StringBuilder指定位置改变字符,使用sb.setCharAt(left, sb.charAt(right))
  3. 单词反转, 判断单词的起始位置,调用字符串翻转。

4.5 剑指58 左旋转字符串 🧡

  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

最后就可以达到左旋n的目的,而不用定义新的字符串,完全在本串上操作。

4.6 KMP

求子串问题

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

4.6.2 459 重复的子字符串 🧡

5. Stack/Queue

5.1 232 用栈实现队列 💚

使用两个栈来实现队列,一个是进栈stackin,一个是出栈stackout

  • offer(): push到stackin
  • poll()/peek(): 看stackout是否为空,不空直接拿。 如果空,则把stackin中元素全部放入stakout后,再拿
  • empty(): 看stackin和stackout是不是都空

5.2 225 用队列实现栈 💚

使用一个queue实现

  • push():offer(),每次有新元素加入queue中时,把除了新元素以外的所有旧元素都依次拿出,并重新放入
  • pop()/peak():直接在queue中拿
  • empty():看queue是不是空

5.3 20 有效的括号 💚

见到左边,把右边放进栈。

  1. 遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
  2. 遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
  3. 已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

5.4 1047 删除字符串中所有相邻重复项 💚

用栈来存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素,相同就删除,不同就添加。

最后把栈中存放的打印,记得reverse!

5.5 150 逆波兰数 🧡

把数字放入stack,遇到计算符号时pop出两个数,运算完成之后重新放入stack。注意-和/的特殊处理

leetcode 内置jdk的问题,不能使用==判断字符串是否相等: 用"+".equals(tokens[i])

放入stack时,String转Integer Integer.valueOf()

5.6 239 滑动窗口最大值 ❤️ ⛽️

单调队列。使用Dequeue

根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点

  1. 队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
  2. 保证每次放进去的数字要比末尾的都大,否则也弹

因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了

注意res数组中存放的是下标而非数值!

5.7 347 前K个高频元素 🧡 ⛽️

优先级队列 PriorityQueue

/*Comparator接口说明:
 * 返回负数,形参中第一个参数排在前面;返回正数,形参中第二个参数排在前面
 * 对于队列:排在前面意味着往队头靠
 * 对于堆(使用PriorityQueue实现):从队头到队尾按从小到大排就是最小堆(小顶堆),
 *                                从队头到队尾按从大到小排就是最大堆(大顶堆)--->队头元素相当于堆的根节点
 * */
class Solution {
    //解法1:基于大顶堆实现
    public int[] topKFrequent1(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
        for(int num:nums){
            map.put(num,map.getOrDefault(num,0)+1);
        }
        //在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2)->pair2[1]-pair1[1]);
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){//大顶堆需要对所有元素进行排序
            pq.add(new int[]{entry.getKey(),entry.getValue()});
        }
        int[] ans = new int[k];
        for(int i=0;i<k;i++){//依次从队头弹出k个,就是出现频率前k高的元素
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
    //解法2:基于小顶堆实现
    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
        for(int num:nums){
            map.put(num,map.getOrDefault(num,0)+1);
        }
        //在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){//小顶堆只需要维持k个元素有序
            if(pq.size()<k){//小顶堆元素个数小于k个时直接加
                pq.add(new int[]{entry.getKey(),entry.getValue()});
            }else{
                if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                    pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                    pq.add(new int[]{entry.getKey(),entry.getValue()});
                }
            }
        }
        int[] ans = new int[k];
        for(int i=k-1;i>=0;i--){//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
}

6. 二叉树

6.1 遍历

6.1.1 递归遍历前中后(recursion)

  • 前(144):left之前
  • 中(94):left,right中间
  • 后(145):right之后

6.1.2 迭代遍历前中后(stack)

  • 前:使用stack,把节点放入同时记录,然后先放节点的右孩子,再放节点的左孩子。output:中左右
  • 后:使用stack,把节点刚入同时记录,先放节点左孩子,再放节点右孩子。ouptut:中右左。然后翻转整个结果:左右中
  • 中:访问和处理节点不同,所以操作更麻烦。先把节点的左孩子一直放入,如果左孩子空了,就弹出同时记录,再把当前节点设置成弹出节点的右孩子。使if/else ⛽️

6.1.3 102 层序遍历 💚

使用queue。循环while套while。第一while是queue非空,说明还有元素。第二while是记录一层。先queue的长度,长度就是本层的元素个数。弹出同时记录,然后把弹出元素的左右孩子都放入queue中。

6.1.4 226 翻转二叉树 💚

前序或者后续处理都可以。处理时,交换该节点的左右孩子。

6.2 101 对称二叉树 💚

使用递归

  • 返回条件:判断该节点的左右孩子

    • 左空,右空,不对称,return false
    • 左不空,右空,不对称 return false
    • 左右都空,对称,返回true
    • 左右都为空,比较节点数值,不相同就return false
  • 单层递归逻辑:

    • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
    • 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
    • 如果左右都对称就返回true ,有一侧不对称就返回false 。

6.2.1 100 相同的树 💚

跟上题一样的判断逻辑

6.2.2 572 另一棵树的子树 💚⛽️

不需要找相同的点,只需要把每一个节点都放进去比就可以。

6.3 104 二叉树最大深度 💚

本题可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始)

而根节点的高度就是二叉树的最大深度,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度。

  • 确定单层递归的逻辑:先求它的左子树的深度,再求右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。

6.3.1 559 N叉树的最大深度 💚

逻辑相同,注意每个点遍历的写法

6.4 111 二叉树最小深度 💚

直觉上好像和求最大深度差不多,其实还是差不少的。依然使用后序遍历。求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。

求最小深度时,要确定此节点的左右孩子都是空时,才能返回。如果左空右不空,返回右高度+ 1,反之亦然。

6.5 222 完全二叉树节点个数 🧡 ⛽️

针对完全二叉树的解法: 使用满二叉树,满二叉树的结点数为:2^depth - 1(2 << depth) - 1

使用后序遍历,使用递归,递归到该节点是满二叉树,判断标准是该节点左子树深度等于右子树深度。

6.6 110 平衡二叉树 💚 ⛽️

  1. 明确递归函数的参数和返回值
  • 参数:当前传入节点。

  • 返回值:以当前传入节点为根节点的树的高度。

  • 那么如何标记左右子树是否差值大于1呢?

  • 如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。

  • 所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。

  1. 明确终止条件
  • 递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
  1. 明确单层递归的逻辑
  • 如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。

  • 分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。

6.7 257 二叉树所有路径 💚

递归中带着回溯

  1. 递归函数参数以及返回值:要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值

  2. 确定递归终止条件:找到叶子节点时终止。叶子节点判断:左右孩子都空。每次进入递归时,把节点加入path。所以现在到叶子节点,要把所有path中的数都加入StringBuilder。之后再加入本节点的值。之后把整个StringBuilder 放入result中。return。

  3. 确定单层递归逻辑: 因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。所以递归前要加上判断语句,下面要递归的左右孩子节点是否为空。递归永远要和回溯在一起,所以每次递归完成后,就要回溯,把最后的叶子节点拿出来。

6.8 404 左子叶之和 💚

本题的重点在于,不能通过节点本身确定是不是左子叶,要通过父亲节点确定。

  1. 确定递归函数的参数和返回值: 判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int。使用题目中给出的函数就可以了。

  2. 确定终止条件 : 如果遍历到空节点,那么左叶子值一定是0。return 0。

  3. 确定单层递归的逻辑:当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。左叶子节点判断: root.left != null && root.left.left == null && root.left.right == null

6.9 513 找树左下角的值 🧡

层序遍历,记录最后一行的第一个值。

6.10 112 路径总和 💚

  1. 确定递归函数的参数和返回类型: 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

  2. 确定终止条件 : 如果到了叶子节点(左右孩子都为空)

    • 如果是目标和,return true
    • 如果不是,直接return false;
  3. 确定单层递归的逻辑: 因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。

    • 值放入
    • 递归过程
    • 值拿出

注意:

  1. 对根节点的处理:因为不管哪一条路径都会一定包含根节点。所以直接加入根节点就可以。process辅助函数中,传入target - root.val。
  2. 正计数不方便。用反向计数。即减法。如果到叶子节点,且count = 0。就是找到了

6.10.1 113 路径总和2 🧡

与上题逻辑一样。就是注意符合要求时,要return res.add(new ArrayList<>(path))。其次是,每次加减都要加减值,且把这个数放入和拿出path

6.11 构造二叉树

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

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

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

逻辑与上面一样。使用前序的第一个数切割中序。

6.12 654 最大二叉树 🧡

  1. 确定递归函数的参数和返回值:参数传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针

  2. 确定终止条件:

    • 传入数组长度小于1: 无法构建返回null
    • 传入数组长度等于1: 说明到叶子节点,new出新节点并返回
  3. 确定单层递归的逻辑:

    • 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组
    • 最大值所在的下标左区间 构造左子树
    • 最大值所在的下标右区间 构造右子树

6.13 617 合并二叉树 💚

  1. 确定递归函数的参数和返回值:首先要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点

  2. 终止条件:

    • 如果t1 == NULL 了,两个树合并就应该是 t2 了(如果t2也为NULL也无所谓,合并之后就是NULL)。
    • 反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)
  3. 单层递归逻辑:

    • 把两个数的值加在一起
    • t1 的左子树是:合并 t1左子树 t2左子树之后的左子树
    • t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树
    • 最终t1就是合并之后的根节点

6.14 700 二叉搜索树 💚

利用二叉搜索树的特点,左边都比节点小,右边都比节点大

6.15 98 验证二叉搜索树 🧡

中序遍历下,输出的二叉搜索树节点的数值是有序序列。验证搜索二叉树就是判断一个序列是否递增。且注意二叉搜索树中不能有重复元素。

一直更新maxValue,发现不符合时返回。为了避免数据类型带来的错误,可以使用pre节点。

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

跟上题思路一样。也是用pre节点记录值就可以。

6.17 501 二叉搜索树中的众数 💚

依旧使用pre节点辅助。

  • 循环中,如果于pre节点数值相同,则count++,不同count变为1
  • 如果count是MAXcount,把节点放入res中
  • 如果count > MAXcount,清空res,把当前count变成MAXcount,然后把当前节点放入答案。

6.18 538 把二叉搜索树变成累加树 🧡

依旧使用pre节点辅助。中序反向遍历搜索二叉树,先右后左。节点加累加前面的值。

6.19 236 二叉树最近公共祖先 🧡

后序遍历,先处理左右,最后处理根节点。要搜索整个树

  • 搜索整个树:

     left = 递归函数(root->left);  // 左
     right = 递归函数(root->right); // 右
     leftright的逻辑处理;         // 中 
    
  • 搜索一条边:

    if (递归函数(root->left)) return ;
    
    if (递归函数(root->right)) return ;
    
  1. 确定递归函数返回值和参数
  2. 确定终止条件:空返回空,等于p或q,说明找到了。三种情况都可以返回root
  3. 单层递归逻辑:
    • 左空右空:返回空
    • 左空右不空:返回右
    • 左不空右空:返回左
    • 左不空右不空:返回本身

6.20 235 二叉搜索树最近公共祖先 🧡

  1. 确定递归函数返回值和参数:
  2. 确定终止条件:空返回空
  3. 单层递归逻辑:二叉搜索树寻找最近的公共祖先祖先,就是寻找第一个出现在两个限定条件范围内的那个值。
    • 如果当前节点的值比限定都小,搜索右树
    • 如果当前节点的值比限定都大,搜索左树
    • 如果都不是,说明找到了,就返回root

6.21 701 二叉搜索树的插入操作 🧡

不需要重构树,找到合适空位插入即可

  1. 确定递归函数返回值和参数:
  2. 确定终止条件:空时,说明找到插入的位置,建立新节点并返回
  3. 单层递归逻辑:
    • 二叉搜索树,节点值>插入值,往左找
    • 节点值<插入值,往右找
    • 下一层将加入节点返回,本层用root->left或者root->right将其接住

6.22 450 删除二叉搜索树中的节点 🧡

  1. 确定递归函数返回值和参数:
  2. 确定终止条件:空时,返回空
  3. 单层递归逻辑:
      1. 左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
      1. 删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
      1. 删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
      1. 左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
    • 二叉搜索树,所以当前值小于目标往右走,大于往左走
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if(root == null){
            return null;
        }
        if(root.val > key){
            root.left = deleteNode(root.left, key);
        }else if(root.val < key){
            root.right = deleteNode(root.right, key);
        }else{
            if(root.left == null && root.right == null){
                return null;
            }
            if(root.right == null){
                return root.left;
            }
            if(root.left == null){
                return root.right;
            }
            TreeNode cur = root.right;
            while(cur.left != null){
                cur = cur.left;
            }
            cur.left = root.left;
            return root.right;
        }
        return root;
    }
}

6.23 669 修建二叉搜索树 🧡

  1. 确定递归函数返回值和参数:
  2. 确定终止条件:空时,返回空
  3. 单层递归逻辑:
    • 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
    • 如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
    • 接下来要将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。
    • 最终返回root

6.24 108 有序数组变搜索二叉树 💚

7. 递归 Recursion

递归模版

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

7.1 组合

7.1.1 77 组合

7.1.2 17 电话号码的组合

7.1.3 39 组合总和

7.1.4 40 组合总和2

7.1.5 216 组合总和3

7.2 分割

7.2.1 131 分割回文串

7.2.2 93 复原IP地址

7.3 子集

7.3.1 78 子集

7.3.2 90 子集2

7.4 排列

7.4.1 46 全排列

7.4.2 47 全排列2

7.4 棋盘

7.4.1 51 N皇后

7.4.2 27 解数独

491 递增子序列 332 重新安排行程

8. Dynamic Programming!!!

基础理论:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

8.1 基础

8.1.1 509 斐波那契数列

  1. 确定dp数组以及下标的含义:dp[i]的定义为:第i个数的斐波那契数值是dp[i]
  2. 确定递推公式:题目给出状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
  3. dp数组如何初始化:题目给出dp[0] = 0, dp[1] = 1
  4. 确定遍历顺序:从前到后

8.1.2 70 爬楼梯

  1. 确定dp数组以及下标的含义:dp[i]: 爬到第i层楼梯,有dp[i]种方法
  2. 确定递推公式:
    • 首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

    • 还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

    • 那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!

  3. dp数组如何初始化:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义
  4. 确定遍历顺序:从前向后

8.1.3 746 使用最小花费爬楼梯

  1. 确定dp数组以及下标的含义:dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]
  2. 确定递推公式
    • dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。

    • dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。

    • 选最小的

  3. dp数组如何初始化:题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 从 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。所以初始化 dp[0] = 0,dp[1] = 0;
  4. 确定遍历顺序:从前到后遍历cost数组

8.1.4 62 不同路径

  1. 确定dp数组以及下标的含义:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
  2. 确定递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]
  3. dp数组如何初始化:如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
  4. 确定遍历顺序:这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的

8.1.5 63 不同路径2

  1. 确定dp数组以及下标的含义:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
  2. 确定递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]
  3. dp数组如何初始化:因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理。
  4. 确定遍历顺序:这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的

8.1.6 343 整数拆分

  1. 确定dp数组以及下标的含义:dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
  2. 确定递推公式:

可以想 dp[i]最大乘积是怎么得到的呢?

其实可以从1遍历j,然后有两种渠道得到dp[i].

一个是j * (i - j) 直接相乘。

一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。

那有同学问了,j怎么就不拆分呢?

j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

也可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

如果定义dp[i - j] * dp[j] 也是默认将一个数强制拆成4份以及4份以上了。

所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

那么在取最大值的时候,为什么还要比较dp[i]呢?

因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

  1. dp数组如何初始化:这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1

  2. 确定遍历顺序: 确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。

注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。

j的结束条件是 j < i - 1 ,其实 j < i 也是可以的,不过可以节省一步,例如让j = i - 1,的话,其实在 j = 1的时候,这一步就已经拆出来了,重复计算,所以 j < i - 1

至于 i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。

8.1.7 96 不同的二叉搜索树

  1. 确定dp数组以及下标的含义:1到i为节点组成的二叉搜索树的个数为dp[i]
  2. 确定递推公式: 在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]

j相当于是头结点的元素,从1遍历到i为止。

所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

  1. dp数组如何初始化:从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。

从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。

所以初始化dp[0] = 1 5. 确定遍历顺序:首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。

那么遍历i里面每一个数作为头结点的状态,用j来遍历。

8.2 背包

8.2.1 01背包

8.2.1.1 01背包理论基础

  1. 确定dp数组以及下标的含义:

    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  2. 确定递推公式: 那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。代表放入物品i时,i-1中随便选的容量,只能是j-weight[i]

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  1. dp数组如何初始化

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品

  1. 确定遍历顺序: 都可以,先遍历物品更好理解
8.2.1.2 01背包滚动数组

思想:对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

读到这里估计大家都忘了 dp[i][j]里的i和j表达的是什么了,i是物品,j是背包容量。

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  1. 确定dp数组以及下标的含义:在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

  2. 确定递推公式:dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?

dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  1. dp数组如何初始化:dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

  2. 确定遍历顺序:二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次! 。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组历的时候不用倒序呢?

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!)

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

(这里如果读不懂,就再回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)

所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的! ,这一点大家一定要注意。

8.2.1.3 416 分割等和子集
  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。
8.2.1.4 1049 最后一块石头的重量

和上题一样