都是一片阴影

833 阅读24分钟

个人算法总结

DFS、BFS、递归与回溯

回溯的本质

回溯的本质:其实就是穷举,回溯问题一般分为 ①组合问题 组合不强调顺序 1,2 2,1代表同一个 ②排列问题 排列强调顺序 ③切割问题 ④子集问题

回溯法的模板

回溯的数据结构其实一般都是树形结构,所以和二叉树的递归很像

回溯的终止条件 一般为path.length==digits.length 或者是start==s.length

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

回溯的遍历过程: 回溯法一般是在确定的集合中回溯,集合大小决定树的宽度,次数决定树的高度

组合类问题

组合类问题的去重 [...path].sort().join() 最优解!

一般组合类问题去重通过map,然后对path进行sort操作,拼接字符串,但这样容易产生bug,你sort以后pop的就不是对应的值了。解决办法,[...path],生成一个副本用来去重,不影响原来的path数组

 if (sum == target) {
            let newRes = [...path].sort((a, b) => a - b).join("");
            if (!map.has(newRes)) {
              map.set(newRes);
              return res.push([...path]);
            }
          }

组合一

第一种写法 只需要一次for循环,不需要自己循环第一层 start是你要递归的下一层,你每层要递归添加啥数据到path里需要自己判断(比如电话号码那道题)

     /* 给定两个整数n和k,返回1-n中所有的k个数的组合 */
      function fs(n, k) {
        let res = [];
        let path = [];
        function dfs(path, start) {
          if (path.length == k) {
            return res.push([...path]);
          }
          for (let i = start; i <= n; i++) {
            path.push(i);
            //dfs递归的时候,把当前一层递归进行,我之前那种方式就是自己多写一次for循环而已,把最上面一层fo循环写出来
            dfs(path, i + 1); //递归下一层的
            path.pop();
          }
        }
        dfs(path, 1);
        console.log(res);
      }
      fs(4, 2);

这是第二种写法:自己手动添上最外层一次循环

/* 给定两个整数n和k,返回1-n中所有的k个数的组合 */
      function fs(n, k) {
        let res = [];
        let path = [];
        function dfs(root, path, start) {
          path.push(root);
          if (path.length == k) {
            return res.push([...path]);
          }
          for (let i = start; i <= n; i++) {
            dfs(i, path, i + 1);
            path.pop();
          }
        }
        for (let i = 1; i <= n; i++) {
          dfs(i, path, i + 1); 
          path.pop();
        }
        dfs(path, 1);
        console.log(res);
      }
      fs(4, 2);

**两个的区别在于,第一种是每次循环这一层,找出对应的数据,然后添进去,再递归下一层,方法更好 第二种:每次找出数据,然后推进去,再循环下一次,先找到数据了,再循环

**

组合三

一般来说 切割类问题其实就是组合三这类问题,一个是和满足,一个是切出来的是否满足条件

   function fs(k, n) {
        //在1-9中找出和为n的k个数的组合,因为是组合 所以顺序不重要
        //结束条件
        let path = [];
        let res = [];
        function dfs(path, start, sum) {
          if (sum > n) {
            return;
          }
          if (sum == n && path.length == k) {
            return res.push([...path]);
          }
          for (let i = start; i <= 9; i++) {
            path.push(i);
            dfs(path, i + 1, sum + i);
            path.pop();
          }
        }
        dfs(path, 1, 0);
        console.log(res);
      }
      fs(3, 9);
      
      第二种: 最外侧调用的时候sum其实默认就是0,所以等同于let sum=0 dfs(path,1,sum)
      //而用dfs(path,i+1,sum+i)不会影响到原来的sum,只会影响参数
        var combinationSum3 = function (k, n) {
        let res = []
        let path = []
        function dfs(path, start, sum) {
          if (sum > n) return
          if (sum == n && path.length == k) {
            return res.push([...path])
          }
          for (let i = start; i <= 9; i++) {
            path.push(i)
            dfs(path, i + 1, sum + i)
            path.pop()
          }
        }
        dfs(path, 1, 0)
        console.log(res)
      }
      
     

电话号码的组合 这是很经典的告知start的作用,每一层需要递归啥元素

这里的每一次循环遍历的是map中对应的比如map[2] 所以start的作用是找出对应的map值,而start+1是找下一个map的值 开始位置都是从0到字符串长度

  var letterCombinations = function (digits) {
        //首先这是一个dfs 搜索,找出所有的子序列
        const map = {
          2: "abc",
          3: "def",
          4: "ghi",
          5: "jkl",
          6: "mno",
          7: "pqrs",
          8: "tuv",
          9: "wxyz",
        };
        let res = [];
        let path = [];
        function dfs(path, start) {
          if (path.length == digits.length) {
            return res.push([...path].join(""));
          }
          //找出对应的字符  digits[0] digits[1]
          //for循环遍历你需要知道自己遍历的是什么,比如这道题,你需要遍历的是digits[0]中对应的map中的字符串,至于start是你的下一级即到了digits[1]
          const letters = map[digits[start]];
          for (let i = 0; i < letters.length; i++) {
            path.push(letters[i]);
            dfs(path, start + 1);
            path.pop();
          }
        }
        dfs(path, 0);
        return res;
      };

组合二

      var combinationSum2 = function (candidates, target) {
        /*
        依然是找出组合,而且和为target,唯一一个难点在于 有重复的元素,且每个元素只能用一次,而且不能包含重复的组合
        */
        candidates.sort((a, b) => a - b);
        let path = [];
        let res = [];
        let map = new Map();
        function dfs(path, start, sum) {
          if (sum == target) {
            let str = path.join("");
            if (!map.has(str)) {
              map.set(str);
              res.push([...path]);
            }
            return;
          }
          if (sum > target) return;
          for (let i = start; i < candidates.length; i++) {
            path.push(candidates[i]);
            dfs(path, i + 1, sum + candidates[i]);
            path.pop();
          }
        }
        dfs(path, 0, 0);
        return res;
      };

括号生成(无回溯)

这是一个特殊的组合,需要找出所有的子节点,但是不需要回溯。 因为只有左右两个括号,两种情况,那么就dfs两次,分别统计,只要不满足条件return即可,满足条件添加,所以不需要回溯了

 let res = [];
        function dfs(path, left, right) {
          if (right > left) return;
          if (left > n) {
            return;
          }
          if (path.length == 2 * n) {
            return res.push(path);
          }
          dfs(path + "(", left + 1, right);
          dfs(path + ")", left, right + 1);
        }
        dfs("", 0, 0);
        console.log(res);

组合总和

题目很简单,无重复的数组,数字可以选任意次,可以从当前的继续选,也可以每次从头开始选,只不过从头开始选需要进行一个去重

  var combinationSum = function (candidates, target) {
        /*
        数字可以重复使用 只需要满足即可  因为是组合,所以还是需要 去重的,只需要每次从自己开始即可
        */
        let path = [];
        let res = [];
        let map = new Map();
        function dfs(path, sum) {
          if (sum == target) {
            let newRes = [...path].sort((a, b) => a - b).join("");
            if (!map.has(newRes)) {
              map.set(newRes);
              return res.push([...path]);
            }
          }
          if (sum > target) return;
          for (let i = 0; i < candidates.length; i++) {
            path.push(candidates[i]);
            dfs(path, candidates[i] + sum);
            path.pop();
          }
        }
        dfs(path, 0);
        console.log(res);
      };
      combinationSum([2, 3, 5], 8);

分割类问题

其实切割类问题类似于组合问题三

组合:选取一个a后,在剩余bcdef中依次选取,然后选了b,再在cdef中选切割。

切割一个a后,在剩余的bcdef中切割,然后切割了b,再在cdef中切割

分割回文串

这里的start,代表了每一次开始切割的位置,dfs(path,i+1)是下一次递归从下一个位置开始切割

      var partition = function (s) {
        let res = [];
        let path = [];
        function dfs(path, start) {
          //start是切割开始的位置
          if (start == s.length) {
            //即超出了
            return res.push([...path]);
          }
          for (let i = start; i < s.length; i++) {
            //开始切割 从开始的地方依次切割后面的
            let str = s.slice(start, i + 1);
            if (isPali(str)) {
              path.push(str);
              //只有是回文的时候再从下一个开始继续递归
              dfs(path, i + 1);
              path.pop();
            } else {
              continue;
            }
          }
        }
        dfs(path, 0);
        return res;
        function isPali(str) {
          let arr = str.split("");
          let resStr = arr.reverse().join("");
          if (str == resStr) {
            return true;
          } else {
            return false;
          }
        }
      };

复原IP地址

 var restoreIpAddresses = function (s) {
        //很明显的切割问题
        let path = [];
        let res = [];
        function dfs(path, start) {
          if (path.length > 4) {
            //防止栈溢出
            return;
          }
          if (start == s.length && path.length == 4) {
            return res.push([...path]);
          }
          for (let i = start; i < start + 3; i++) {
            //这个不就跟分割回文串一样的嘛,只不过判断条件不一样而已
            let str = s.slice(start, i + 1);
            if (str < 0 || str > 255) {
              continue;
            }
            if (str.length > 1 && str[0] == "0") {
              continue;
            }
            path.push(str);
            dfs(path, i + 1);
            path.pop();
          }
        }
        dfs(path, 0);
        console.log(res);
      };

子集类问题

所有的子集

错误办法:用for循环无限迭代,每一个for循环找出一个对应的元素值,可以用这种办法找出所有的子数组,通过slice,子集是可跳过的

 var subsets = function (nums) {
        /*
        暴力搜索  无限嵌套for循环,很垃圾  子数组可以用这个方法
        */
        let res = [];
        for (let i = 0; i < nums.length; i++) {
          for (let j = i + 1; j < nums.length; j++) {
            //子集中元素为2可以使用
            res.push([nums[i], nums[j]]);
          }
        }
      };

分割类问题和子集问题的区别:

1、分割类问题每找出一个元素,for循环根据start,继续dfs剩余的所有元素,直至叶子节点

2、子集是只要找到一个元素,就推到res数组中

其实也没什么区别,只不过推入res的时机不一样,分割或者组合问题都是需要满足某个条件,比如匹配完所有,最后把叶子推入到res中,所以一般在结束条件if中进行。而子集问题是你每经历一个节点就要推入,最后结束

      var subsets = function (nums) {
        let res = [[]];
        let path = [];
        function dfs(path, start) {
          //终止条件
          if (start == nums.length) {
            return;
          }
          for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);
            res.push([...path]);
            dfs(path, i + 1);
            path.pop();
          }
        }
        dfs(path, 0);
        return res;
      };

子集二 集合中含有重复元素,结果中不包含

首先需要进行排序,避免414和144不同,然后就跟子集一的做法一样,只不过推入res数组的时候需要判断一下是否存在map中了

      /*
      解集中不包含重复的子集,即去重      */
      var subsetsWithDup = function (nums) {
        nums.sort((a, b) => a - b);
        //注意啊,这样的做法需要排序,因为排序后才能保证 414 就是144
        let map = new Map();
        let path = [];
        let res = [];
        function dfs(path, start) {
          if (start == nums.length) {
            return;
          }
          for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);
            let str = path.join("");
            //进行去重,如果不去重直接res.push()
            //这里通过判断是否存在map里,如果不存在map里,才推入res
            if (!map.has(str)) {
              map.set(str, str);
              res.push([...path]);
            }

            dfs(path, i + 1);
            path.pop();
          }
        }
        dfs(path, 0);
        res.push([]);
        return res;
      };

递增子序列

这题和子集二很像,子集2要返回所有不重复的子集,所以会先排序,然后去重 但是这题不行,因为它需要递增子序列,如果排序之后都是递增的。

需要自己手动判断当前项大于最后一项

有个问题就是如果不存在w要默认为0 undefined无法比较大小,boolean(undefined<1)永远是false

     var findSubsequences = function (nums) {
        let path = [];
        let res = [];
        let map = new Map();
        function dfs(path, start) {
          for (let i = start; i < nums.length; i++) {
            //需要大于之前的
            let w = path[path.length - 1] || 0;
            if (nums[i] >= w) {
              path.push(nums[i]);
              let str = path.join("");
              if (!map.has(str) && path.length >= 2) {
                map.set(str, str);
                res.push([...path]);
              }
              dfs(path, i + 1);
              path.pop();
            }
          }
        }
        dfs(path, 0);
        return res;
      };

排列类问题

全排列

找出[1,2,3]所有的全排列 无重复数字 两种:1、通过use数组判断比较拉胯。2、直接includes>

这种方法前提是没有重复元素,所以可以includes判断,但是全排列二有重复元素了,自然不能用第2种方法。
     function fs(nums) {
        //返回没有重复数字的全部排列,每次重头来,然后去重即可
        let res = [];
        let path = [];
        let map = new Map();
        function dfs(path) {
          if (path.length == nums.length) {
            return res.push([...path]);
          }
          for (let i = 0; i < nums.length; i++) {
            if (path.includes(nums[i])) {
              continue;
            }
            path.push(nums[i]);
            dfs(path);
            path.pop();
          }
        }
        dfs(path);
        return res;
      }
      fs([1, 2, 3]);

全排列二

[1,2,2]找出所有的全排列 这题的难点在于:首先有重复元素,自然不能用之前的第三种方式。所以只能用use数组判断是否有用过当前元素,用过就跳过。最后还需要进行一个去重,因为重复元素可能有重复排列

      function fs(nums) {
        let path = [];
        let res = [];
        let use = [];
        let map = new Map();
        function dfs(path, use) {
          if (path.length == nums.length) {
            let str = path.join("");
            if (!map.has(str)) {
              map.set(str);
              res.push([...path]);
            }
          }
          for (let i = 0; i < nums.length; i++) {
            if (!use[i]) {
              use[i] = true;
              path.push(nums[i]);
              dfs(path, use);
              path.pop();
              use[i] = false;
            }
          }
        }
        dfs(path, use);
        console.log(res);
      }
      fs([1, 2, 2]);

链表类相关问题

链表问题主要解决办法:一、反转链表从局部到整体 2、设置快慢指针 3.合并两个链表

快慢指针

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

思路:先定义一个快指针,让它走n步,然后让慢指针和快指针同时走,直至快指针走到最后一个,这时候slow.next就是要删除的那个节点 slow.next=slow.next.next

     let list = {
        val: 1,
        next: {
          val: 2,
          next: {
            val: 3,
            next: {
              val: 4,
              next: {
                val: 5,
                next: null,
              },
            },
          },
        },
      };
      var removeNthFromEnd = function (head, n) {
        //思路 :删除链表的倒数第n个节点,可以用快慢指针的方式
        //让快指针先走n步,然后两个指针同时走。这样slow.next就是要删除的那个节点
        let slow = head;
        let fast = head;
        for (let i = 0; i < n; i++) {
          fast = fast.next;
        }
        //fast指针不存在
        if (!fast) {
          //说明删除的是头节点
          return slow.next;
        }
        while (fast && fast.next) {
          //让fast指针走到最后
          slow = slow.next;
          fast = fast.next;
        }
        slow.next = slow.next.next;
        return head;
      };

合并列表


合并两个有序列表

合并两个有序列表很简单,因为可以新建一个头节点,依次比对每个节点的大小,进行拼接

重排链表

不需要对节点进行比较,只需要按照规则进行重排即可

先裁减成两个链表,然后反转,while(head2)存在,每次先找出对应的next1和next2,指都指向head

   var reorderList = function (head) {
        //首先需要裁减这个链表,然后进行反转
        let p = head;
        let total = 0;
        let map = new Map();
        while (p) {
          map.set(total++, p);
          p = p.next;
        }
        let w = parseInt(total / 2);
        let head2 = map.get(w).next;
        head2 = reverse(head2);
        let dummy = new ListNode();
        dummy.next = head;
        let next1 = null;
        let next2 = null;
        while (head2) {
          next1 = head.next;
          next2 = head2.next;
          head.next = head2;
          head = next1;
          head2.next = head;
          head2 = next2;
        }
        return dummy;
        function reverse(head) {
          let cur = head;
          let pre = null;
          let next = null;
          while (cur) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
          }
          return pre;
        }
      };

合并K个升序链表

比较纯的办法:解构所有链表,拼接成数组排序,然后再生成链表

var mergeKLists = function (lists) {
        //合并k个升序列表有几种方式 1.记录每一个链表节点的值,然后重写一个新链表
        let path = [];
        for (let i = 0; i < lists.length; i++) {
          let head = lists[i];
          while (head) {
            path.push(head.val);
            head = head.next;
          }
        }
        path.sort((a, b) => a - b);
        let node = new ListNode();
        let p = node;
        for (let i = 0; i < path.length; i++) {
          let n = new ListNode(path[i], null);
          node.next = n;
          node = node.next;
        }
        return p.next;
      };

动态规划dp

动态规划思想

动态规划:某一问题是由多个重叠的子问题构成,就可以使用动态规划。

动态规划的某一个状态一定是由上一个状态推断而出,贪心没有状态推导,而是从局部选出最优

动态规划的解题步骤:

①确定dp数组,还有其下标的含义

②确定递推公式

③初始化dp

④确定遍历顺序

使用最小花费爬楼梯

①设置dp[i] 表示爬到第i层所需的最少的体力,可以得出当前dp[i]依赖于dp[i-1]或者dp[i-2]加上他们所爬楼梯的体力 ②状态转移方程 dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

   var minCostClimbingStairs = function (cost) {
        /* 
        ①设dp[i]为爬上i层台阶所耗费的最少的体力
        ②状态转移方程  dp[i]可以从dp[i-1]或者dp[i-2]爬上来  dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
        ③初始化状态  dp[0]=0 dp[1]=0 可以从0或1开始,不需要耗费体力
        */
        let dp = new Array(cost.length + 1)
        dp[0] = 0
        dp[1] = 0
        for (let i = 2; i <= cost.length; i++) {
          dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
        }
        return dp[dp.length - 1]
      }

不同路径

 /* 
      机器人从左上角到右下角的不同路径 
      ①设置dp[i][j] 表示从(0,0)到第(i,j)位置所有的不同路径
      它从上一个节点到i,j 只能往下或者往右走一步,得到状态转移方程
      ②dp[i][j]=dp[i-1][j] (往右走) +  dp[i][j-1] (往下走)
      ③初始化状态方程  当i==0的时候,只能往右走 j==0的时候只能往下走
      */
      var uniquePaths = function (m, n) {
        let dp = new Array(m)
        for (let i = 0; i < dp.length; i++) {
          dp[i] = new Array(n)
        }
        for (let i = 0; i < m; i++) {
          dp[i][0] = 1
        }
        for (let j = 0; j < n; j++) {
          dp[0][j] = 1
        }
        for (let i = 1; i < m; i++) {
          for (let j = 1; j < n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
          }
        }
        return dp[m - 1][n - 1]
      }

不同路径Ⅱ

这题有个细节在于,初始化dp的时候,如果横纵两条遇到了石头,必须直接赋值为0,跳出循环。

  /*
      ① dp[i][j]表示到(i,j)的格子的不同路径
      ② 状态转移方程  因为只能下或者右 dp[i][j]=dp[i-1][j]+dp[i][j-1]
      但是如果obstacleGrid[i][j]==1即有障碍物,那么dp[i][j]=0
      ③初始化 i,j==0的时候路径 没遇到石头都是1 遇到了 后面都是0

       */
      var uniquePathsWithObstacles = function (obstacleGrid) {
        let m = obstacleGrid.length
        let n = obstacleGrid[0].length
        let dp = new Array(m)
        for (let i = 0; i < m; i++) {
          dp[i] = new Array(n).fill(0)
        }
        for (let i = 0; i < m; i++) {
          if (obstacleGrid[i][0] == 1) {
          //初始化的时候遇到了石头
            dp[i][0] = 0
            break
          }
          dp[i][0] = 1
        }
        for (let j = 0; j < n; j++) {
          if (obstacleGrid[0][j] == 1) {
            dp[0][j] = 0
            break
          }
          dp[0][j] = 1
        }
        for (let i = 1; i < m; i++) {
          for (let j = 1; j < n; j++) {
            if (obstacleGrid[i][j] == 1) {
              dp[i][j] = 0
            } else {
              dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
            }
          }
        }
        return dp[m - 1][n - 1]
      }

整数拆分

①:定义状态 dp[i]表示整数i拆分以后,得到的最大结果

②:状态转移方程 i可以拆分为两个数字,dp[i]=j*(j-i),当然j-i这个剩余部分还可以继续拆分,从中找出最大的dp[i]=j*dp[j-i]

③:初始化状态 dp[0]=0 dp[1]=0 dp[2]=1 (1*1)

④:确定遍历, 从左往右,i从3开始,j从1到一半即可

重点:为什么每次都要与dp[i]比较,因为底层j每次从1开始拆分,拆分一次的最大结果保存到之前的dp[i]中,所以需要比较每次j的结果

    /*
      ①定义状态 dp[i]表示数字i拆分后,得到的最大乘积。那把i拆成几个数字呢?
      ②举例 10可以拆分成2 8 这种两个数字的 dp[i]=j*(i-j),那么8其实也可以继续拆 dp[i]=j*dp[i-j]即拆成多个数字的
      j是从1开始遍历,拆分j   
      得到状态转移方程 dp[i]=Math.max(j*(i-j),j*dp[i-j],dp[i])
      ③初始化状态 dp[2]才有意义 dp[2]=1  i从3开始 j从1开始,每次<=parseInt(i/2)一般即可

      */
      var integerBreak = function (n) {
        let dp = new Array(n + 1).fill(0)
        dp[2] = 1

        for (let i = 3; i <= n; i++) {
          for (let j = 1; j <= parseInt(i / 2); j++) {
            //至于为什么要与dp[i]进行比较,因为你每次是j从1开始拆分的啊,然后j一直到最后,得出一个最大的结果作为dp[i]的值
            //括号里的dp[i]是上一次j得出的最大的结果
            dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j])
            
            //dp[i]=Math.max(dp[i],j*(i-j))
            //dp[i]=Math.max(dp[i],dp[i-j]*j) dp[i]是之前的结果
          }
        }
        return dp[dp.length - 1]
      }
      integerBreak(10)

不同的二叉搜索树

这题的重点在于,当前节点的二叉搜索树的个数=左子树节点二叉搜索树的个数*右子树节点的二叉搜索树的个数

image.png

image.png

     /*
      ①定义状态dp[i] 表示i个节点构成的不同的二叉搜索树的个数
      ②状态转移方程:
      当前可以确定的是如果一个节点,只有一颗,两个节点,有两颗
      当前节点的二叉搜索树的个数=左节点的二叉搜索树的个数* 右节点的二叉搜索树的个数
      比如i==3  dp[3]=(1为节点)dp[0]*dp[2]+dp[1]*dp[1]+dp[2]*dp[0]
      因为扣除了一个根节点,所以j从1开始
      dp[i]+=dp[j-1][i-j]


       */
      var numTrees = function (n) {
        let dp = new Array(n + 1).fill(0)
        dp[2] = 2
        dp[1] = 1
        /* 注意dp[0]必须赋初值为1 不然*永远是0了 */
        dp[0] = 1
        for (let i = 3; i <= n; i++) {
          for (let j = 1; j <= i; j++) {
            dp[i] += dp[j - 1] * dp[i - j]
          }
        }
        console.log(dp[n])
      }
      numTrees(5)

背包问题

背包问题解题模板

01背包:

①求装满背包的最大价值。外层物品,内层背包容量,逆向遍历背包。

②求装满背包有几种组合方式。外层物品、内层背包容量,逆向遍历背包。细节在于:初始化dp[0]=1 dp[j]+=dp[j-weight[i]]

完全背包:

①任取n件物品,求装满背包的最大价值。外层物品,内层背包容量,注意是正向遍历背包

for(let i=0;i<weight.length;i++){
    for(let j=weight[i];j<=bagSize;j++){
        dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])
    }
}

②求装满背包有几种组合方式。组合:外层物品,内层背包,正向遍历,初始化dp[0]=1。dp[j]+=dp[j-weight[i]]

let dp=new Array(bagSize+1).fill(0)
dp[0]=1
for(let i=0;i<weight.length;i++){
 for(let j=weight[i];j<=bagSize;j++){
     dp[j]+=dp[j-weight[i]]
 }
}

③求装满背包有几种排列方式。排列:外层背包,内层物品,需要if条件判断,dp[j]+=dp[j-weight[i]]。排列j从0开始,其他j都是weight[i]

let dp=new Array(bagSize+1).fill(0)
dp[0]=1
for(let j=0;j<=bagSize;j++){
    for(let i=0;i<weight.length;i++){
        if(j>=weight[i]){
            dp[j]+=dp[j-weight[i]]
        }
    }
}

④求装满背包的最少物品数量外层物品,内层背包。细节:初始化为Infinity,dp[0]=0,dp[j]=Math.min(dp[j],dp[j-weight[i]]+1)

let dp=new Array(bagSize+1).fill(Infinity)
dp[0]=0
for(let i=0;i<weight.length;i++){
    for(let j=weight[i];j<=bagSize;j++){
            dp[j]=Math.min(dp[j],dp[j-weight[i]]+1)
    }
}

01-背包

01背包:二维dp数组dp[i][j] 表示 从下标(0,i)的物品里任取,放进容量为j的背包的最大价值

二维递推公式: dp[i]依赖于前一个状态,如果当前第i件物品不取,那么dp[i][j]=dp[i-1][j]

dp[i]依赖于前一个状态,dp[i-1](dp[i]表示下标从0-i的物品任取,dp[i-1]表示从(0,i-1)的物品任取,即前一个)。

1如果不放物品i,dp[i][j]=dp[i-1][j]

2如果放物品i,dp[i][j]=dp[i-1][j-weight[i]]+value[i]

初始化状态: 当j==0,背包容量为0,不能放,自然是0,当dp[0][j] j>=weight[0],那么价值就是do[0][j]=value[0]

      /*
      weight=[1,3,4]
      value=[15,20,30]
      背包容量bagSize   0-bagSize
      */
      function fs(weight, value, bagSize) {
        //初始化dp数组 dp[i][j]表示从(0,i)件物品里任取,放到背包容量为j的背包,价值最大为dp[i][j]
        //为什么要有weight.length+1 因为可以不取物品 比如i==1 那么其实取的是weight[0]第一件物品  所以第一行都是0
        let dp = new Array(weight.length + 1)
        for (let i = 0; i < dp.length; i++) {
          dp[i] = new Array(bagSize + 1).fill(0)
        }
        for (let j = weight[0]; j <= bagSize; j++) {
          dp[1][j] = value[0]
        }
        for (let i = 2; i <= weight.length; i++) {
          for (let j = 0; j <= bagSize; j++) {
            if (j >= weight[i - 1]) {
            //注意,你要取的是第i件物品,但是它下标其实是i-1
              dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1])
            } else {
              dp[i][j] = dp[i - 1][j]
            }
          }
        }
        console.log(dp)
      }

image.png

滚动数组降维,一维

01背包 重点:外层物品,内层背包逆序遍历

//当前dp[i][j]只与dp表的上一行有关
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])

每一次都保存了上一次dp[i][j]的值,然后下一次刷新整个数组

dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])

分割等和子集

思路:需要将nums拆分成两个数组,其和相等

转化:转换成任取nums里的数字,每个只能取一次,最后和等于sum的一半即可

背包容量即为sum/2 最后只需要判断装满背包的价值是否等于sum/2即可

      /*
      分割nums数组,使得两个子集和相等
      */
      var canPartition = function (nums) {
        nums.sort((a, b) => a - b)
        let sum = nums.reduce((pre, item) => {
          return (pre += item)
        }, 0)
        if (sum % 2 != 0) return false
        let target = sum / 2
        /*
        转换思路,取nums里的数字,每个只能取一次。
        背包容量即为target,重量即为nums,价值也为nums,背包装满后,判断其最大价值,如果最大价值等于背包容量,说明结果为true
        
        j==target的时候,价值dp[j]是否等于target即可
        */
        let bagSize = target
        let dp = new Array(bagSize + 1).fill(0)
        for (let i = 0; i < nums.length; i++) {
          for (let j = bagSize; j >= nums[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
          }
        }
        //最后装满的时候,判断价值是否等于target即可
        if (dp[bagSize] == target) {
          return true
        } else {
          return false
        }
      }
      canPartition([1, 5, 11, 5])

最后一块石头的重量Ⅱ

   /*
      转化思路:先计算出背包容量 即sum/2 ,然后装满这个背包,找出其最大价值。然后结果sum-最大价值*2 就是剩余的
      最后×2 因为是两个背包
      */
      var lastStoneWeightII = function (stones) {
        let sum = stones.reduce((p, i) => (p += i))
        let bagSize = parseInt(sum / 2)
        let dp = new Array(bagSize + 1).fill(0)
        for (let i = 0; i < stones.length; i++) {
          for (let j = bagSize; j >= stones[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])
          }
        }
        return sum - dp[bagSize] * 2
      }
      lastStoneWeightII([2, 7, 4, 1, 8, 1])

目标和

这题不难,重点是判断条件和转化思路。你需要转化成组合成装满left有几种组合方式

   var findTargetSumWays = function (nums, target) {
        /* 
        任取nums数组里的数字,每个只能取一次,结果要为target。
        转化思路:全部加的和认为是left  全部减的是right
        left+right=target
        left-right=sum
        left=(target+sum)/2
        即背包容量为left,求装满背包有几种组合方式
        */
        let sum = nums.reduce((p, i) => (p += i))
        if ((target + sum) % 2) {
          return 0
        }
        if(Math.abs(target)>sum) return 0
        let bagSize = (sum + target) / 2
        let dp = new Array(bagSize + 1).fill(0)
        /* 注意,组合dp[0]=1 */
        dp[0] = 1
        for (let i = 0; i < nums.length; i++) {
          for (let j = bagSize; j >= nums[i]; j--) {
            dp[j] += dp[j - nums[i]]
          }
        }
        return dp[dp.length - 1]
      }

一和零 记忆

这是一道特殊的01背包

 /* 
      strs是物品,可以任取,每个只能取一次
      m,n都是背包容量
      ①dp[i][j]表示有i个0 和j个1的最大子集的长度
      ②递推公式 先计算出当前字符串,有多少个1和0。然后根据这些1和0的个数,找出装满子集的最大长度
      
      */
      var findMaxForm = function (strs, m, n) {
        let dp = new Array(m + 1)
        let zeroNum
        let oneNum
        for (let i = 0; i < dp.length; i++) {
          dp[i] = new Array(n + 1).fill(0)
        }
        for (let str of strs) {
          zeroNum = 0
          oneNum = 0
          for (let c of str) {
            if (c === '0') {
              zeroNum++
            } else {
              oneNum++
            }
          }
          for (let i = m; i >= zeroNum; i--) {
            for (let j = n; j >= oneNum; j--) {
              dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
            }
          }
        }

        return dp[m][n]
      }

完全背包

概念:完全背包和01背包的区别在于,每件物品可以取无限次。也就是遍历顺序的不同。如果非排列,外层遍历物品,内层遍历背包,j>=weight[i]

    function fs(weight, value, bagSize) {
        //定义dp数组,dp[i][j]表示对于(0,i)件物品,装满容量为j的最大价值
        let dp = new Array(bagSize + 1).fill(0)
        for (let i = 0; i < weight.length; i++) {
          for (let j = weight[i]; j <= bagSize; j++) {
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
          }
        }
        console.log(dp)
      }

零钱兑换Ⅱ 组合

      var change = function (amount, coins) {
        /*
        问的是组合方式 coins是物品可以任取,且每个可以取多次,背包容量是amount
        dp[0]=1  dp[j]+=dp[j-weight[i]]
        */
        let bagSize = amount
        let dp = new Array(bagSize + 1).fill(0)
        dp[0] = 1
        for (let i = 0; i < coins.length; i++) {
          for (let j = coins[i]; j <= bagSize; j++) {
            dp[j] += dp[j - coins[i]]
          }
        }
        return dp[dp.length - 1]
      }

组合总和Ⅳ 排列

   var combinationSum4 = function (nums, target) {
        /* 
        注意,这求的是排列数  完全背包求排列 外层背包,内层物品,j从0开始了
        */
        let bagSize = target
        let dp = new Array(bagSize + 1).fill(0)
        dp[0] = 1
        for (let j = 0; j <= bagSize; j++) {
          for (let i = 0; i < nums.length; i++) {
            if (j >= nums[i]) {
              //对于排列,外层是背包容量且j从0开始遍历
              //这也就导致了需要多一个if判断 j>=weight[i]
              dp[j] += dp[j - nums[i]]
            }
          }
        }
        console.log(dp)
      }

爬楼梯 完全背包排列

进阶:n不就是bagSize,物品[1,2] 1,2可以任取,问到楼顶有几种方法即求排列 因为先跳1再跳2 和先2再1不一样

      var climbStairs = function (n) {
        /* 
        爬楼梯最基础的解法 dp[j]=dp[j-1]+dp[j-2]  
    
        */
        let dp = new Array(n + 1).fill(0)
        dp[0] = 1
        let weight = [1, 2]
        //排列,外层背包,内层物品 需要if判断
        for (let j = 0; j <= n; j++) {
          for (let i = 0; i < weight.length; i++) {
            if (j >= weight[i]) {
              dp[j] += dp[j - weight[i]]
            }
          }
        }
        console.log(dp)
      }
      climbStairs(3)

零钱兑换

完全背包,求装满背包的最少物品数。细节,初始化dp的时候只能是Infinity,dp[0]=0 dp[j]=Math.min(dp[j],dp[j-weight[i]]+1)

 var coinChange = function (coins, amount) {
        /*
        完全背包,求的是装满背包的最少的物品数
        dp[j]=Math.min(dp[j],dp[j-weight[i]]+1)
        */
        let bagSize = amount
        let dp = new Array(bagSize + 1).fill(Infinity)
        dp[0] = 0
        for (let i = 0; i < coins.length; i++) {
          for (let j = coins[i]; j <= bagSize; j++) {
            dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1)
          }
        }
        return dp[dp.length - 1] === Infinity ? -1 : dp[dp.length - 1]
      }
      coinChange([1, 2, 5], 11)

完全平方数

 /*
      转化思路:完全平方数就是物品,背包容量就是n 求装满背包的最少物品数
      物品的重量就是 对应的平方
      比如n==12  那么i就可以1 4 9 所以物品就是[1,4,9]  
      */
      var numSquares = function (n) {
        let bagSize = n
        let dp = new Array(bagSize + 1).fill(Infinity)
        dp[0] = 0
        for (let i = 0; i ** 2 <= n; i++) { //1 4, 9
          for (let j = i ** 2; j <= bagSize; j++) {//对应的重量也就是i**2
            dp[j] = Math.min(dp[j], dp[j - i ** 2] + 1)
          }
        }
        return dp[dp.length - 1]
      }
      numSquares(12)

单词拆分 完全背包

  /* 
      从wordDict中选取字符,判断是否能装满s ,其实这是一道很明显的完全背包题目。而且这是有顺序的,所以是排列
      ①dp[j]表示字符长度为j时,可以从wordDict中选取到字符组合成s
      */
      var wordBreak = function (s, wordDict) {
        let bagSize = s.length
        let dp = new Array(bagSize + 1).fill(false)
        dp[0] = true
        for (let j = 0; j <= bagSize; j++) {
          //排列,外层背包,内层物品
          for (let i = 0; i < wordDict.length; i++) {
            if (j >= wordDict[i].length) {
              /* j是背包容量,对应截取的字符是要少1的(0,j-1) */
              let tempStr = s.slice(j - wordDict[i].length, j)
              if (wordDict.includes(tempStr) && dp[j - wordDict[i].length]) {
                //截取的剩余字符要在wordDict中,之前的那一半也要在
                dp[j] = true
              }
            }
          }
        }
        return dp[dp.length - 1]
      }
      wordBreak('applepenapple', ['apple', 'pen'])

打家劫舍

只需要注意状态 dp[i]表示从0到i间房子偷到的最大价值。dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i-1])

第i间房对应的是nums[i-1]

  var rob = function (nums) {
        //设dp[i]从(0,i)间房偷到的最大金额
        //状态转移方程 dp[i]依赖于前天或者是昨天
        /* 
          dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1])
        */
        //初始化 dp[0]=0 i从3开始,dp[1]=nums[0] dp[2]=Math.max(nums[0],nums[1]) dp[3]=Math.max
        let dp = new Array(nums.length + 1).fill(0)
        dp[0] = 0
        dp[1] = nums[0]
        dp[2] = Math.max(nums[0], nums[1])
        for (let i = 3; i < dp.length; i++) {
          dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1])
        }
        return dp[dp.length - 1]
      }

打家劫舍Ⅱ

因为成了环状,偷了第一家就不能偷最后一家。所以只需要把第一家去了,或者最后一家去了,计算两个最大值即可

 var rob = function (nums) {
        /* 
        每个房子都围成了一圈 ,其实就是两种情况。
        第一种偷第一家,去除掉最后一家。第二种去除掉第一家,然后看是否偷最后一家
        
        ①dp[i]表示从(0,i)间房子偷到的最大价值
        ②递推,当前状态依赖于前两个,如果前天偷,那么今天偷,如果昨天偷,那么今天不能偷
        dp[i]=Math.max(dp[i-2]+nums[i-1],dp[i-1])
        ③初始化 dp[0]=0  dp[1]=nums[0] dp[2]=Math.max(nums[0],nums[1])
        */
        if (nums.length == 1) return nums[0]
        if (nums.length == 2) return Math.max(nums[0], nums[1])
        let nums1 = nums.slice(1)
        let nums2 = nums.slice(0, nums.length - 1)
        function fs(nums) {
          let dp = new Array(nums.length + 1).fill(0)
          dp[1] = nums[0]
          dp[2] = Math.max(nums[0], nums[1])
          for (let i = 3; i < dp.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1])
          }
          return dp[dp.length - 1]
        }
        let s1 = fs(nums1)
        let s2 = fs(nums2)
        return Math.max(s1, s2)
      }
      console.log(rob([1, 2, 3, 1]))

打家劫舍Ⅲ

思路1: 虽然它是树形的,用层序遍历,然后dp,但又有问题,两个子节点都可以偷。所以错误

思路2: 用后序遍历,先找到叶子节点,left[0]表示不偷左子节点的最大值,left[1]表示偷了左子节点的最大值。找出最大的,然后向上返回即可

var rob = function (root) {
        //需要后序遍历从叶子节点开始
        //[偷了当前节点的最大值,不偷当前节点的最大值]
        function postOrder(root) {
          if (!root) return [0, 0]
          //left为左子节点,right为右子节点 left[0]为不偷左子节点。left[1]为偷左子节点
          let left = postOrder(root.left)
          let right = postOrder(root.right)
          //偷了根节点的话,那么左右子节点不能偷
          let withRoot = root.val + left[0] + right[0]
          let outRoot = Math.max(left[0], left[1]) + Math.max(right[0], right[1])
          return [outRoot, withRoot]
        }
        let arr = postOrder(root)
        return Math.max(...arr)
      }

最长有效括号

对于这种括号匹配问题,第一思路就是用栈 匹配

思路1:遇到左括号入栈, 遇到右括号出栈,那么长度为当前索引-出栈的那个左括号的索引+1

比如索引为5 时,2出栈 长度为5-2+1 但是,当遇到6的时候,又该出栈,而且最长有效括号应该是0-5

image.png

解决思路:提前存入一个-1遇到右括号的时候,让栈顶元素出栈。比如5,让2出栈,栈顶元素为-1 长度为当前索引-出栈以后栈顶元素。如果遇到了6,-1出栈,那么判断栈是否为空,如果空,让6入栈更新栈顶元素,和-1一样,作为标记

  var longestValidParentheses = function (s) {
        //定义一个栈,同时存入-1作为标记  它就类似于一个)
        let stack = [-1]
        let max = 0
        for (let i = 0; i < s.length; i++) {
          if (s[i] == '(') {
            //左括号直接索引入栈
            stack.push(i)
          } else {
            //遇到右括号,先出栈一个,再找栈顶元素
            stack.pop()
            if (stack.length) {
              let temp = i - stack[stack.length - 1]
              max = Math.max(temp, max)
            } else {
              //如果标记都出栈了,那么就重新入栈
              stack.push(i)
            }
          }
        }
        return max
      }

最小路径和

这题其实和不同路径很相似,dp[i][j]表示第i、j个格子所走的最小的路径, 因为只能向下走和向右走,所以状态转移方程:dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j]

 var minPathSum = function (grid) {
        let m = grid.length
        let n = grid[0].length
        let dp = new Array(m)
        for (let i = 0; i < dp.length; i++) {
          dp[i] = new Array(n).fill(0)
        }
        dp[0][0] = grid[0][0]
        for (let i = 1; i < m; i++) {
          dp[i][0] = dp[i - 1][0] + grid[i][0]
        }
        for (let j = 1; j < n; j++) {
          dp[0][j] = dp[0][j - 1] + grid[0][j]
        }
        for (let i = 1; i < m; i++) {
          for (let j = 1; j < n; j++) {
            dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
          }
        }
        console.log(dp)
      }

编辑距离 hard

这题和正则表达式有点像:但是更简单,因为最外层不同时,只有三种操作添加删除和修改

定义状态:dp[i][j]表示字符长度为i的word1转化成字符长度为j的word2的最少操作次数。所以dp[i][j]依赖于前面一个状态

如果word1[i-1]==word2[j-1],那么dp[i][j]=dp[i-1][j-1]

如果word1[i-1]!=word2[j-1],那么有三种情况,1、在word1中添加一个字符让它们相同,那么 dp[i][j]=dp[i][j-1]+1 2、删除,word1中删除一个,转化成word2中添加一个字符 3、直接修改

 var minDistance = function (word1, word2) {
        //将word1变为word2,可以添加删除或替换
        /* 
        设dp[i][j]表示字符从i变到j的最少操作次数
        如果j==0 那么只需要i次即可,删除
        同理i==0 只需要添加即可
        
        这题和正则表达式有点像:但是更简单,因为最外层不同时,只有三种
        添加删除和修改
        dp[i][j]表示字符从word1(0-i)到word2(0-j)的最少操作次数,
        所以状态依赖于前一项dp
        一、如果word1[i-1]==word2[j-1],那么dp[i][j]=dp[i-1][j-1]
        二、如果word1[i-1]!=word2[j-1],分三种情况,添加删除或修改
           1、添加操作,即在word1中添加一个字符让它和word2[j-1]相同
           dp[i][j]=dp[i][j-1]+1
           2、删除操作,即在word1中删除一个字符,但是等同于在word2中添加一个字符
           dp[i][j]=dp[i-1][j]+1
           3、修改
           即直接添加一次
           dp[i][j]=dp[i-1][j-1]+1
        */
        let m = word1.length;
        let n = word2.length;
        if (m == 0 && n == 0) return 0;
        let dp = new Array(m + 1);
        for (let i = 0; i < dp.length; i++) {
          dp[i] = new Array(n).fill(0);
        }
        for (let i = 1; i <= m; i++) {
          dp[i][0] = i;
        }
        for (let j = 1; j <= n; j++) {
          dp[0][j] = j;
        }
        for (let i = 1; i <= m; i++) {
          for (let j = 1; j <= n; j++) {
            if (word1[i - 1] == word2[j - 1]) {
              dp[i][j] = dp[i - 1][j - 1];
            } else {
              dp[i][j] = Math.min(
                dp[i][j - 1] + 1,
                dp[i - 1][j] + 1,
                dp[i - 1][j - 1] + 1
              );
            }
          }
        }
        return dp[m][n];
      };

正则表达式匹配hard

定义状态 dp[i][j]表示字符串长度为i的 s(0,i-1)字符串是否与长度为j的p(0,j-1)匹配。

如果末尾s[i-1]==p[j-1]相等,不考虑*,当前状态依赖于前面的字符串是否匹配 即 s(0,i-2)是否与p(0,j-2)匹配----->dp[i][j]=dp[i-1][j-1]

  /* 
      s='aa' p='a*'
      定义dp[i][j]为s(0,i-1)的字符串与p(0,j-1)的字符串是否匹配。
      ①不考虑* 如果s[i-1]==p[j-1]||p[j-1]=='.'即最外层匹配了,那么转换成小问题,判断里面的是否匹配即dp[i-1][j-1]是否匹配
      dp[i][j]=dp[i-1][j-1] 
      ②考虑*   如果p[j-1]=='*',即不用考虑p[j-1]直接考虑s[i-1]与p[j-2]。
         a 如果s[i-1]==p[j-2]||p[j-2]=='.'  那么*可以让p[j-2]重复0,1,多次,这要这三者任意一种情况能让它为true即可,所以用||
          a.1  如果s='aab'  p='aabb*' s[i-1]==p[j-2],让它干点一个b,这时候dp[i][j]就依赖于前面的aab与aab是否匹配,需要比较
          s(0,i-1)与p(0,j-3)是否匹配
          dp[i][j]=dp[i][j-2] 
          a.2  s='aab' p='aab*' 因为s[i-1]与p[j-2]匹配,只需要一个,这时候依赖于s(0,i-2)是否与p(0,i-3)匹配
          dp[i][j]=dp[i-1][j-2]    b*==b
          a.3  s='aabbb'p='aab*'  *让b重复多次   可以换种思路,让s串删除几个 因为s[i-1]==p[j-2],所以只需要s(0,i-2)是否与
          p(0,j-1)是否匹配,因为重复多次,等价于多次删除,*还要再考虑
          dp[i][j]=dp[i-1][j]

          b 如果s[i-1]!=p[j-2]  这也别怕,因为p[j-1]=='*' 可以让p[j-2]重复0次直接考虑s(0,i-1)是否与p(0,j-3)是否匹配
      */
        var isMatch = function (s, p) {
          let m = s.length
          let n = p.length
          let dp = new Array(m + 1)
          for (let i = 0; i < dp.length; i++) {
            dp[i] = new Array(n + 1).fill(false)
          }
          dp[0][0] = true
          //初始化状态,当s为空串的时候,p串p[j-1]如果是*,那么依赖于p(0,j-3)是否匹配,因为P[j-2]被删除了
          for (let j = 1; j <= n; j++) {
            if (p[j - 1] == '*') {
              dp[0][j] = dp[0][j - 2]
            }
          }
          for (let i = 1; i <= m; i++) {
            for (let j = 1; j <= n; j++) {
              if (s[i - 1] == p[j - 1] || p[j - 1] == '.') {
                //比较s(0,i-2)是否与
                dp[i][j] = dp[i - 1][j - 1]
              } else if (p[j - 1] == '*') {
                if (s[i - 1] == p[j - 2] || p[j - 2] == '.') {
                  //重复多次,即删除s,比较s(0,i-2)与p(0,j-1)
                  dp[i][j] = dp[i][j - 2] || dp[i - 1][j - 2] || dp[i - 1][j]
                } else {
                  //s[i-1]!=p[j-2],删除p[j-2] 比较s(0,i-1)与p(0,j-3)
                  dp[i][j] = dp[i][j - 2]
                }
              }
            }
          }
          return dp[m][n]
        }

买卖股票

买卖股票的最佳时机Ⅰ

这是最基础的:只需要遍历每一次,找出当前天的最小的收入,然后判断当前天卖出价格就行。

      var maxProfit = function (prices) {
        /* 
        只能在一天买入,后面的某一天卖出
        */
        let profit = 0
        let min = Infinity
        for (let i = 0; i < prices.length; i++) {
          min = Math.min(min, prices[i])
          profit = Math.max(prices[i] - min, profit)
        }
        return profit
      }

字符串相关问题

无重复字符的最长子串

思路:肯定用滑动窗口,定义一个left左指针,如果遇到了重复字符,就让left跳到之前的那个字符的下一个 比如abba,就让left从a跳到b。 但有个细节在于,map.get(s[i])>=left,因为不能回溯过去,比如abbbcca,如果没有这个条件,最后left还会跳到a,然后每次遍历的时候都判断长度即可

      var lengthOfLongestSubstring = function (s) {
        //因为是子串,所以可以用滑动窗口,或者直接找出所有的子串
        /* 
        有一种情况是 没有重复的 因此遇到重复的计算 错误 
        转化思路:每遍历一个字符,就计算一次长度,如果遇到了重复的,就直接让它移动到重复的下一位
        */
        let maxLength = 0
        let left = 0
        let map = new Map()
        for (let i = 0; i < s.length; i++) {
          //遇到重复字符后不能回头,每次替换的时候必须>=之前已经调整过的left
          //比如abbcdea 如果没有右边条件 left就变成了1了回头了
          if (map.has(s[i]) && map.get(s[i]) >= left) {
            left = map.get(s[i]) + 1
          }
          maxLength = Math.max(maxLength, i - left + 1)
          map.set(s[i], i)
        }
        return maxLength
      }

字母异或

这题的难度不大。遍历字符串数组,获取每一项字符串,然后细节在于对字符串排序只能str.sort() 不能(a,b)=>a-b。然后生成一个数组,map中存入 {排序后的字符串=>生成的数组},如果有相同的直接取出数组push即可

var groupAnagrams = function (strs) {
        let map = new Map();
        let res = [];
        //遍历字符串数组,然后将每个字符串进行排序,存到map中
        for (let i in strs) {
          let strsArr = strs[i].split("").sort().join("");
          if (!map.has(strsArr)) {
            let list = [strs[i]];
            res.push(list);
            map.set(strsArr, list);
          } else {
            let list = map.get(strsArr);
            list.push(strs[i]);
          }
        }
        return res;
      };

最小覆盖字串 hard

leedcode连接

解决思路:滑动窗口。定义一个map,用来存储字符串t所缺失的字符和数量。如果对应的字符数量为0,那么就代表不缺失该字符了,missType--

优化滑动窗口:当misstype==0的时候,代表滑动窗口内部找齐了所有字符,然后找出最小的字符串比较。再将滑动窗口右移,即把left对应的字符从滑动窗口中滑出,滑出时需要判断是否是map中的字符,是的话需要++,如果==0就让missType+1

有个细节在于,start初始化的时候赋值为Infinite,因为如果s='AB' t='a',那么不匹配,start=0,返回的不是空串,定义为Infinte可以多一次判断

      var minWindow = function (s, t) {
        let map = new Map() //定义一个map用来存储目前所缺失,需要寻找的字符和个数
        let missingType = 0 //目前滑动窗口内缺失的字符种类  因为可能是BAAC 这种情况,所以只能通过种类数量来判断是否满足
        let minLength = Infinity //minLength和start都是为了最后返回字符串
        let start = Infinity //start赋值Infinity是为了最后如果没有匹配的,那么它就不会进入
        for (let i = 0; i < t.length; i++) {
          if (!map.has(t[i])) {
            map.set(t[i], 1)
            missingType++
          } else {
            map.set(t[i], map.get(t[i]) + 1)
          }
        }
        //定义的滑动窗口
        let left = 0
        let right
        for (right = 0; right < s.length; right++) {
          if (map.has(s[right])) {
            //找到了一个目标字符,那么所需要的数量--
            map.set(s[right], map.get(s[right]) - 1)
          }
          if (map.get(s[right]) == 0) {
            //如果对应的个数为0,代表当前的滑动窗口内已经凑满所需要的对应字符,那么类别就--
            missingType--
          }
          while (missingType == 0) {
            if (right - left + 1 < minLength) {
              minLength = right - left + 1
              start = left
            }
            //找出当前的最小长度,然后滑动窗口需要右移
            let leftChar = s[left]
            if (map.has(leftChar)) {
              map.set(leftChar, map.get(leftChar) + 1)
            }
            //为什么是大于0呢 ,因为滑动窗口中可能存放了多个相同字符,map的值就是负的,只有大于0时,才是缺失
            if (map.get(leftChar) > 0) {
              missingType++
            }
            left++
          }
        }
        if (start == Infinity) return ''
        return s.slice(start, start + minLength)
      }

二叉树

二叉树的理论基础

二叉搜索树:左节点一定小于根节点,右节点一定大于根节点

平衡二叉树:左右节点的高度差小于1

Snipaste_2023-01-05_14-41-26.png

二叉树的遍历方式

深度优先遍历:前序遍历、中序遍历、后序遍历。迭代法遍历 广度优先遍历:一般是通过队列来实现的

二叉树的深度优先遍历 即dfs

二叉树的dfs是特殊的,它必须先push,因为确定的知道只有两个节点,要继续深度遍历下一个节点,无法像组合一样循环遍历的时候push。

总结:组合子集排列分割:条件 循环内部满足条件的push,然后继续递归,然后pop。二叉树则是先push,然后判断是否满足条件,然后直接递归左右子树

一、迭代方式dfs实现,找出二叉树所有的路径,其实就是先序遍历转换下
 function fn(node) {
        //dfs遍历出二叉树的所有路径
        let path = []
        let res = []
        function dfs(node, path) {
          if (!node) return
          path.push(node.val)
          if (!node.left && !node.right) {
            return res.push([...path])
          }
          if (node.left) {
            dfs(node.left, path)
            path.pop()
          }
          if (node.right) {
            dfs(node.right, path)
            path.pop()
          }
        }
        dfs(node, path)
        console.log(res)
      }
      fn(binaryTree)
二、找出所有的节点,即用先序遍历或者栈的方式
//用栈的方式其实很简单,将根节点入栈,循环弹出,判断左右子节点是否存在,存在的话!!
从右往左依次入栈。这个可以用来遍历children类型的树(非二叉树也可以,二叉树就用先序呗)
 function stackMethod(root) {
        if (!root) return
        const stack = []
        stack.push(root)
        while (stack.length > 0) {
          let node = stack.pop()
          console.log(node.val)
          //从后往前,将右、左子树依次入栈
          if (node.right) {
            stack.push(node.right)
          }
          if (node.left) {
            stack.push(node.left)
          }
        }
      }
      stackMethod(binaryTree)
      

children 树 递归dfs

   let tree = {
        name: '0',
        children: [
          { name: '2', children: [{ name: '4' }] },
          { name: '1', children: [{ name: '5', children: [{ name: '11' }] }, { name: '7' }] }
        ]
      }
      function dfs(root) {
        //递归
        if (!root) return
        console.log(root.name)
        let children = root.children || []
        for (let i = 0; i < children.length; i++) {
          dfs(children[i])
        }
      }
      dfs(tree)

栈stack

     function dfs(root) {
        const stack = [root]
        while (stack.length) {
          let node = stack.pop()
          console.log(node.name)
          let children = node.children || []
          for (let i = children.length - 1; i >= 0; i--) {
            stack.push(children[i])
          }
        }
      }
      dfs(tree)

二叉树的层序遍历 即bfs (无论是路径还是节点,都是用队列的方式)

多叉树的层序遍历

      let tree = {
        name: '0',
        children: [
          { name: '2', children: [{ name: '4' }] },
          { name: '1', children: [{ name: '5', children: [{ name: '11' }] }, { name: '7' }] }
        ]
      }
        function bfs(root) {
        if (!root) return
        const queue = []
        queue.push(root)
        while (queue.length > 0) {
          let len = queue.length
          //必须一次性将当前节点全出队列!不然(找路径会有问题,遍历不会),然后出了以后再将子节点入队列
          for (let i = 0; i < len; i++) {
            let res = queue.shift()
            console.log(res.name)
            let children = res.children || []
            for (let i = 0; i < children.length; i++) {
              queue.push(children[i])
            }
          }
        }
      }
      bfs(tree)

bfs找出每一层路径,其实也是用队列的方式,注意bfs必定是含有null的,不要null的话结果过滤就行

这就是为什么需要len,一次性将队列中的所有节点出队列。如果没有len,没有这个for循环,每一次节点出队列后,它都会重新push一个[],导致都是一个个单独的,至于直接获取节点值则没有这个问题

bfs含有null


     function bfs(root) {
        if (!root) return
        let res = []
        let queue = [root]
        while (queue.length) {
          let len = queue.length
          res.push([])
          for (let i = 0; i < len; i++) {
            let node = queue.shift()
            if (!node) {
              res[res.length - 1].push(null)
            } else {
              res[res.length - 1].push(node.val)
              queue.push(node.left)
              queue.push(node.right)
            }
          }
        }
        console.log(res)
      }

bfs不含有null 写法1

最后一层的[5,6,7,8]进入循环,这时候node依然存在,依然会push node.left只不过是null而已

var levelOrder = function(root) {
    if(!root) return []
    const queue=[root]
    const res=[]
    while(queue.length){
        let len=queue.length
        res.push([])
        for(let i=0;i<len;i++){
            let node=queue.shift()
            if(node){
                res[res.length-1].push(node.val)
                queue.push(node.left)
                queue.push(node.right)
            }
        }
    }
    return res.slice(0,res.length-1)
};

bfs不含null经典写法

//当然推荐第二种写法
  function levelOrder(root) {
          if (!root) return []
          let result = []
          let queue = [root]
          while (queue.length) {
            let len = queue.length
            result.push([])
            for (let i = 0; i < len; i++) {
              let node = queue.shift()
              result[result.length - 1].push(node.val)
              if (node.left) {
                queue.push(node.left)
              }
              if (node.right) {
                queue.push(node.right)
              }
            }
          }
        }

二叉树到叶子节点的所有路径

注意:只要是树相关的路径,需要把path.push()写在前面,因为它们的return 条件和前面的组合排列问题不一样,而且节点可能不存在

错误写法:
let tree = {
        val: 1,
        children: [
          { val: 2, children: [{ val: 5 }, { val: 6 }, { val: 7 }] },
          { val: 3, children: [{ val: 8 }] },
          { val: 4 },
        ],
      }
      let result = []
      function dfs(root, path) {
         if (!children.length) {
          return result.push([...path])
        }
        let children = root.children || []
        for (let i = 0; i < children.length; i++) {
          path.push(root.val)
          dfs(children[i], path)
          path.pop()
        }
      }
      dfs(tree, [])
      console.log(result)
      /* 
      如果通过children.length来return
      path:[1,2] 然后dfs(5,[1,2])这时候children.length不存在了,只能return了。结果就会少了一层
      所以难点在于何时return。
      但是如果通过判断该节点是否存在来return,可以进入5,但是child.length这时候无法进入循环,也无法return
      */
 function dfs(root, path) {
        if (!root) {
          return null
        }
        path.push(root.val)
        if (!root.left && !root.right) {
          return res.push([...path])
        }
        if (root.left) {
          dfs(root.left, path)
          path.pop()
        }
        if (root.right) {
          dfs(root.right, path)
          path.pop()
        }
      }
      dfs(tree, [])
      console.log(res)

多叉树到叶子节点的所有路径

 let tree = {
        val: 1,
        children: [
          { val: 2, children: [{ val: 5 }, { val: 6 }, { val: 7 }] },
          { val: 3, children: [{ val: 8 }] },
          { val: 4 },
        ],
      }
      let result = []
      function dfs(root, path) {
        path.push(root.val)
        let children = root.children || []
        if (!children.length) {
          return result.push([...path])
        }
        for (let i = 0; i < children.length; i++) {
          dfs(children[i], path)
          path.pop()
        }
      }
      dfs(tree, [])
      console.log(result)

例题

获取二叉树某个节点的深度

直接法:
 function getDepth(root) {
        if (!root) return 0
        //为什么要return 0 因为Math.max undefined是无法比较的  null会默认转换成0
        return Math.max(getDepth(root.left), getDepth(root.right)) + 1
      }

二叉树的最大深度 (获取某个节点的深度)

二叉树的最大深度有两种方式。①用bfs找出所有路径,然后返回res.length即可②直接法,找出当前根节点的深度即可

②直接法:
 function getDepth(root) {
        if (!root) return 0
        //为什么要return 0 因为Math.max undefined是无法比较的  null会默认转换成0
        return Math.max(getDepth(root.left), getDepth(root.right)) + 1
      }

    if(!root) return 0
    const queue=[root]
    const res=[]
    while(queue.length){
        let len=queue.length
        res.push([])
        for(let i=0;i<len;i++){
            let node=queue.shift()
            if(node){
                res[res.length-1].push(node.val)
                queue.push(node.left)
                queue.push(node.right)
            }
        }
    }
        return res.slice(0,res.length-1).length
};

前序和中序构造二叉树

结束条件是!inorder.length。有个细节在于root={val:preorder[0]}这么写,不能写成构造函数的方式

preorder: 1,mid+1 mid+1 inorder: 0,mid m id+1

      var buildTree = function (preorder, inorder) {
        if (!inorder.length) return null
        //找到根节点 然后找出在中序遍历中的下标
        let root = {
          val: preorder[0],
          left: null,
          right: null
        }
        const mid = inorder.indexOf(preorder[0])
        root.left = buildTree(preorder.slice(1, mid + 1), inorder.slice(0, mid))
        root.right = buildTree(preorder.slice(mid + 1), inorder.slice(mid + 1))
        return root
      }

中序与后序构造二叉树

细节 这是!postorder.length不存在 为终止条件 inorder: 0,mid mid+1 postorder:0,mid mid,postorder.length-1

  var buildTree = function (inorder, postorder) {
        if (!postorder.length) return null
        let root = {
          val: postorder[postorder.length - 1],
          left: null,
          right: null
        }
        let mid = inorder.indexOf(postorder[postorder.length - 1])
        root.left = buildTree(inorder.slice(0, mid), postorder.slice(0, mid))
        root.right = buildTree(inorder.slice(mid + 1), postorder.slice(mid, postorder.length - 1))
        return root
      }

不同的二叉搜索树 纯背公式

这题就是纯背了 每个节点 numTrees(i)*numTrees(n-i-1)

var numTrees = function (n) {
        if (n == 0 || n == 1) {
          return 1
        }
        let sum = 0
        for (let i = 0; i < n; i++) {
          sum += numTrees(i) * numTrees(n - i - 1)
        }
        return sum
      }

验证二叉搜索树 中序遍历获取递增数组

这题的陷阱在于不能单纯的遍历每个节点,然后左节点小于根节点,右节点大于

Snipaste_2023-01-09_14-46-29.png

这种情况就是错误的

正确思路:二叉搜索树中序遍历,可以得到一个递增的数组,然后判断,其实不难,重点是思路

var isValidBST = function(root) {
    let arr=[]
    function inorder(root){
        if(!root) return null
        inorder(root.left)
        arr.push(root.val)
        inorder(root.right)
    }
    inorder(root)
    for(let i=1;i<arr.length;i++){
        if(arr[i]<=arr[i-1]){
            return false
        }
    }
    return true
};

二叉树的最大路径和 hard

路径和:当前节点+左子树的最大路径和+右子树的最大路径和。

转换思路,先计算出左右子树的最大路径和,然后层层向上,所以用的是后序遍历。可以先递归获取到叶子节点。如果某个节点的值小于0,那么直接return 0,即忽略跳过该节点,最后return 的时候,当前节点的最大路径值:只能是左右子树中的最大值+root.val 当前节点的最大路径值:只能是左右子树中的最大值+root.val Snipaste_2023-01-13_12-53-32.png

   var maxPathSum = function (root) {
        let maxSum = root.val
        /* 
        从任意一个节点出发,找出最大路径。
        当前节点的路径和=root.val+左子树的最大路径和+右子树的最大路径和
        如果当前某个节点的值小于0,那么就直接返回0,忽略该节点
        因为依赖于左右子树的叶子节点,所以只能后序遍历,从下往上进行
        */
        function dfs(root) {
          //如果root不存在,直接return 0 忽略该节点
          if (!root) return 0
          //后序遍历
          let leftVal = dfs(root.left)
          let rightVal = dfs(root.right)
          //找出当前节点最大的路径和:root+左子树+右子树
          const innerMathSum = leftVal + root.val + rightVal
          maxSum = Math.max(maxSum, innerMathSum)
          //递归,返回的当前节点可获取的路径的最大值,即左右子树选一边
          let outputSum = Math.max(0, leftVal, rightVal) + root.val
          return outputSum > 0 ? outputSum : 0
        }
        dfs(root)
        return maxSum
      }

双指针 三指针问题

三指针

颜色分类

定义三个指针 left:用来保存所有的0 right:用来保存2 index:用来循环遍历,当index遇到0的时候,就和left进行交换,同时left+1,如果遇到了1,那么就跳过,因为最左边一定是0,最右边一定是2

如果index遇到了2,那么和right进行交换,注意right-- 但是index不能++,因为交换回来的不能确定是1还是2还是0,需要再次判断

因此,循环终止条件index<=right,这是因为right右侧一定都是2,如果index>right了,那么直接会进入第三个else if 就会交换,导致错误

 var sortColors = function (nums) {
        let left = 0;
        let right = nums.length - 1;
        //让left永远指向0  让right永远指向2
        //index用来遍历
        let index = 0;
        while (index <= right) {
          //还有个细节在于 index<=right  因为right到最右端,一定是为2的,
          //如果index>right,那么必定跳到nums[index]==2的条件里,然后交换
          if (nums[index] == 0) {
            [nums[index], nums[left]] = [nums[left], nums[index]];
            left++;
            index++;
          } else if (nums[index] == 1) {
            index++;
          } else if (nums[index] == 2) {
            [nums[index], nums[right]] = [nums[right], nums[index]];
            // index++; 不能index++ 这是因为交换后的值你不确定是2还是0还是1
            //需要再次判断
            right--;
          }
        }
        return nums;
      };
      console.log(sortColors([2, 0, 1]));

岛屿问题

单词搜索

这题和岛屿问题一样,都是找到对应的值,然后dfs遍历四周。

结束条件:当i,j越界,或者已经遍历过了即为0,或者与word[index]不符合

只有当index遍历到最后一位,且经历了上一个if,因此return true

需要暂存board[i][j]然后遍历四周,只有四周有一个true即可,最后需要返回给它 再return res

      var exist = function (board, word) {
        let m = board.length
        let n = board[0].length
        function dfs(i, j, index) {
          if (i >= m || i < 0 || j >= n || j < 0 || board[i][j] == 0 || board[i][j] != word[index]) {
            return false
          }
          if (index == word.length - 1) {
            return true
          }
          let temp = board[i][j]
          board[i][j] = 0
          //遍历周围,判断是否有对应的值
          let res = dfs(i - 1, j, index + 1) || dfs(i + 1, j, index + 1) || dfs(i, j - 1, index + 1) || dfs(i, j + 1, index + 1)
          board[i][j] = temp
          return res
        }
        for (let i = 0; i < m; i++) {
          for (let j = 0; j < n; j++) {
            if (board[i][j] == word[0]) {
              let res = dfs(i, j, 0)
              if (res) {
                return true
              }
            }
          }
        }
        return false
      }