个人算法总结
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)
不同的二叉搜索树
这题的重点在于,当前节点的二叉搜索树的个数=左子树节点二叉搜索树的个数*右子树节点的二叉搜索树的个数
/*
①定义状态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)
}
滚动数组降维,一维
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
解决思路:提前存入一个-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
解决思路:滑动窗口。定义一个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
二叉树的遍历方式
深度优先遍历:前序遍历、中序遍历、后序遍历。迭代法遍历 广度优先遍历:一般是通过队列来实现的
二叉树的深度优先遍历 即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
}
验证二叉搜索树 中序遍历获取递增数组
这题的陷阱在于不能单纯的遍历每个节点,然后左节点小于根节点,右节点大于
这种情况就是错误的
正确思路:二叉搜索树中序遍历,可以得到一个递增的数组,然后判断,其实不难,重点是思路
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
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
}