1、时间复杂度、空间复杂度

时间复杂度:表示代码的运行时间,通过代码的执行次数来表示
1 T(n) = 3n,时间复杂度是 T(n) = o(n),例如:一个 for 循环
2 T(n) = 5logn,时间复杂度是 T(n)=O(logn),例如:每次都/2,逐级递减
3 T(n) = 2,时间复杂度是 T(n) = O(1)
4 T(n) = 0.5 n n + 0.5n,时间复杂度是 T(n) = O(n²),例如:2 个 for 循环
5 T(n) = nlogn,时间复杂度为 O(nlogn),例如:每次/2,外层再来个 for 循环

递归一般都是 O(n)

空间复杂度:对一个算法在运行过程中临时占用存储空间大小的一个量度,反映的对内存占用的趋势,而不是具体内存
详解时间复杂度和空间复杂度

1、数组

1.1、二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function binarySearch(arr, target) {
let l = 0;
let r = arr.length - 1;
while (l <= r) {
let mid = Math.floor((l + r) / 2);
if (arr[mid] == target) {
return mid;
}
if (arr[mid] < target) {
l = mid + 1;
}
if (arr[mid] > target) {
r = mid - 1;
}
}
return -1;
}
console.log(binarySearch([0, 1, 2, 3, 4, 5], 4));

1.2 快排

1.3 归并排序

利用双指针 while,一个数组一个指针

例题

有序数组内两个数字和为 target,并且返回索引值(167)

方法一:暴力解法双循环
方法二:利用二分查找法+target-arr[i]
方法三:对撞指针

找到有序数组中最短的连续子数组,子数组之和>=s(209)

注意是连续的
解法一:暴力解法,遍历所有的子数组
解法二:滑动窗口,必须是有序的,两个指针不停+1,取>s 的数组,并且和上一次满足跳转的数组长度做比较

一个字符串中寻找没有重复字母的最长子串(3)

解法一:暴力解法,遍历出所有的子串
解法二:滑动窗口,出现重复,将左侧指针指向重复的索引位置

找出字符串 p,是 s 的子串的索引(438)

注意:可以非连续子串
如: s=”abab” p=”ba” 返回[0,1,2]

解法一:暴力解法,双循环,第二个循环从第 n 位开始(子串长度)
解法二:滑动窗口

给定一个字符串 S 和 T,在 S 中寻找最短的子串,包含 T 中的所有字符(76)

如 S=”ADOBECODEBANC” T=”ABC” 结果为”BANC”

解法一: 滑动窗口

2、查找(map 和 set)

C++中:
map 和 set 底层实现为平衡二叉树
unordered_map 和 unordered_set 为哈希表

2.1 给一个数组(可以无序),返回两个值等于 target 的索引(1)

例如: numbs = [2,7,11,15] target = 9 返回 [0,1]

方法一:暴力解法,遍历所有数据对求和
方法二:先排序,后对撞指针,但是注意保留之前的索引值(存到对象内 key:0,value:值)
方法三:将所有元素放到查找表(有坑,只遍历一次),之后对每个 a,查找 target-a 是否存在就可以了,注意遍历的时候,遍历到第二个相同的元素的时候的处理,不要一上来就都遍历一遍都放到表中(v4-3)

2.2 寻找一个数组中的三个值,使得 a+b+c=0 (15)(18)(16)

这个是 3sum 题,如果改成 4sum 呢 或者更接近值而不是等于

如: numbs = [-1,0,1,2,-1,-4], 结果为[[-1.0,1],[-1,-1,2]]

方法一:暴力解法:遍历 2 遍/3 遍/4 遍循求 2sum 3sum 4sum,注意遍历中索引不能相同
方法二:将其中一遍放到 map 中,减少一层遍历,和 target-sum 对比
方法三:如果是 4sum,先遍历 2 遍(从 3,4 位开始),将两个数的和放到一个 map 中,然后再遍历 2 遍,其中对比 map 中是否有符合要求的

2.3 给出四个数组,寻找 A[i]+B[j]+C[k]+D[l]=0 (454)

拆分成两个双层遍历
第一个双层,将和放到 map 中
第二个双层,判断 map 中是否有 0-A[i]-B[j]

2.4 通过颠倒字符顺序仙的单词进行分组(49)

[“eat”,”tea”,”tan”,”ate”,”nat”,”bat”] 返回 [[“eat”,”tea”,”ate”],[“nat”,”tan”],[“bat”]]

方法一:遍历,将单词排序,然后将 sort 后的顺序最为 key 存到 map 中, 之后每个都排序,在 map 中查找

2.5 有 n 个点,取其中 3 个点使两边相同,求排列数(447)(149)

例如:给 3 个点[[0,0],[1,0],[2,0]] 分别为 i,j,k 要求 ij ==ik
结果:[[1,0],[0,0],[2,0]]和[[1,0],[2,0],[0,0]]

方法一:暴力解法 遍历 3 层
方法二:设定一个为 i 点,然后分别求剩下的点到 i 的距离 v-4-3

1
2
3
4
5
var map = {
距离: '频次',
长度10: '20次',
};
// 拿掉一个点后,其实就是取这个点后还剩一个点,总次数就是 20*19

447代码图

2.6 一个数组,是否存在索引 i 和 j,使 numbs[i] == numbs[j]且 i 和 j 之间的差不超过 k(219)

方法一:暴力解法,循环 2 次
方法二:滑动窗口,先遍历 j 到 k 的长度,如果都没有找到,则滑动窗口进行寻找

3、链表

注意:所有的链表都能转成数组来解决!!!!!!

3.1 每两个相邻的链表交换位置(24)

1、先创建虚拟节点 p
2、标注 node1、node2、next 节点
3、p 挪动到下一次要移动的前边就是 node1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 迭代-视频解法
var swapPairs = function (head) {
const dummyHead = new ListNode(0);
dummyHead.next = head;
let temp = dummyHead;
while (temp.next !== null && temp.next.next !== null) {
const node1 = temp.next;
const node2 = temp.next.next;
// 下边的顺序背下来就行
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1; // 如果只是反转链表这里改成node2就对了,因为node2现在已经变到第一位了,temp指向这里往下
}
return dummyHead.next; // 这里一定是dummyHead
};

// 递归
var swapPairs = function (head) {
if (head === null || head.next === null) {
return head;
}
const newHead = head.next;
head.next = swapPairs(newHead.next);
newHead.next = head;
return newHead;
};

24-1
24-2

3.2 链表排序

可以用上边的迭代方法来进行比较互换

插入排序(147)
归并排序(148)

3.3 删除某个节点(237)

正常是知道前一个节点,直接用前一个节点 next 指向下一个节点就可以了

但是:现在那不到前一个节点
解法:把当前节点直接改成下一个节点,然后将指向下下个节点

3.5 删除倒数第 n 个节点(19)

通常删除节点,要创建一个虚拟节点,避免第一个就是要删除节点

方法一:改为数组实现
方法二:遍历一遍得到长度,然后再遍历一次进行删除
方法三:滑动窗口,虚拟节点(对应要删除前一个节点),q 节点对应 null,一次遍历直到 q 变为 null

19

3.6 L(0)→L(1)→L(2)转换为 L(0)→L(n)→L(1)→L(n-1)(143)

方法一:转化成数组做
方法二:截取一半,将后半部分反转,之后进行拼接

3.7 判断是回文链表(234)

方法一:转换为数组
方法二:快慢指针,取到中间位置,反转后边链表,和前边比较

3.7 链表反转

方法一:转换为数组
方法二:就地反转(迭代)
方法三:递归
方法四:新建一个链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 就地反转-迭代
var reverseList = function (head) {
let [prev, curr] = [null, head];
while (curr) {
let tmp = curr.next; // 1. 临时存储当前指针后续内容
curr.next = prev; // 2. 反转链表
prev = curr; // 3. 接收反转结果
curr = tmp; // 4. 接回临时存储的后续内容
}
return prev;
};
// 上边的更容易理解-下边这个只是把第一项移到了最后
var swapPairs = function (head) {
const dummyHead = new ListNode(0);
dummyHead.next = head;
let temp = dummyHead;
while (temp.next !== null && temp.next.next !== null) {
const node1 = temp.next;
const node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node2; // 这里改成node2就对了,因为node2现在已经变到第一位了,temp指向这里往下
}
return dummyHead.next;
};

4.栈和队列

4.1 判断括号是否合法(20)

方法一:利用栈,判断当前是否是闭合标签,和 pop 出来的最后一个值看是或否配对
方法二:双指针回文串

4.2 简化路径(71)

方法一: 利用栈,创建一个新数组,当发现..的时候,将最后一项 pop 出来

4.3 递归算法 - 树的遍历 - 深度优先遍历 - 前中后序遍历(144,94,145)

除了利用递归外,还可以利用栈来遍历模拟递归过程,类似于广度遍历,创建一个[],然后进行 while+push 操作

4.4 扁平化数组(341)

1
2
3
4
5
6
7
function flatten(arr, result = []) {
for (let item of arr) {
if (Array.isArray(item)) flatten(item, result);
else result.push(item);
}
return result;
}

4.5 队列

基本应用:广度优先遍历(bfs)
树:层序遍历
图: 无权图的最短路径

4.6 二叉树的层序遍历(102)(107)

每次取都取队首的,然后判断是否有子节点,有的话就 push 到队列中,一直循环

4.7 二叉树之子顺序遍历(103)

4.8 返回右侧可以看见的节点(199)

4.9 图的最短路径问题

4.10 寻找最少的完全平方数,使他们的和为 n(279)

例如:
12=4+4+4; 13=4+9

利用图论解决:创建节点和边
建模:具体题解思路
从 n 到 0,每个数字表示一个节点
如果两个数字 x 到 y 相差一个完全平方数,则连接一条边
我们得到一个无权图
原题转化为无权图中从 n 到 0 的最短路径问题

leetcode 建模:
树的数据结构
5
4(5-11) 1(5-22)第二层
3(4-11) 0(4-22) 0(1-1*1) 第三层

利用广度优先遍历,遍历的时候记得存储层数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var numSquares = function (n) {
const list = [];
list.push({ num: n, step: 0 });

while (list.length > 0) {
const { num, step } = list.shift();
if (num === 0) return step;
for (let i = 1; num - i * i >= 0; i++) {
list.push({ num: num - i * i, step: step + 1 });
}
}
};

// 注意优化点
// 因为这个是个图而不是单纯的树,所以num的值num-i*i 会出现大量的重复,所以需要存储
var numSquares = function (n) {
const list = [];
list.push({ num: n, step: 0 });
const visitedObj = { [n]: true };
while (list.length > 0) {
const { num, step, visited } = list.shift();
for (let i = 1; ; i++) {
const extraNum = num - i * i;
if (extraNum < 0) break;
// this line return the result in advance, it reduces perform time very much.
if (extraNum === 0) return step + 1;
if (!visitedObj[extraNum]) {
visitedObj[extraNum] = true;
list.push({ num: num - i * i, step: step + 1 });
}
}
}
};

4.11 单词最短变换路径,单词接龙 (127) (127II)

[‘hot’, ‘dot’, ‘dog’, ‘lot’, ‘log’, ‘cog’,]
hit→cog

方法一:尝试暴力解法,将所有组合都匹配出来
方法二:转化为图,bfs

4.12 优先队列

寻找最大值或最小值,按顺序出列

4.13 给出前 k 个出现频率最高的元素(347)

[1,1,1,2,2,3] k=2 返回[1,2]
方法一:利用 obj 来计数,然后进行排序
方法二:维护优先队列

4.14 k 个有序数组,归并为一个有序数组(23)

5 二叉树和递归

递归条件:(一定要找到这两点)
1、递归终止条件
2、递归过程

利用递归的返回值

5.1 二叉树类型

1、满二叉树:每一层节点数达到最大
2、完全二叉树:除了最后一层,其余都是满的,最后一层还要从左开始排列
3、二叉查找树:左小,右大
4、平衡二叉树:是一颗二叉查找树,并且左右两个子树高度不能超过 1
参考资料

5.2 求一颗二叉书最高的深度(104)

1
2
3
4
5
6
var maxDepth = function (root) {
if (root == null) return 0;
const left = maxDepth(root.left);
const right = maxDepth(root.right);
return Math.max(left, right) + 1;
};

5.3 求一颗二叉树的最低深度(111)

方法一:广度优先遍历
方法二: 递归所有路径高度,比较深度

5.4 反转二叉树(226)

4 → 4
2 7 → 7 2

1
2
3
4
5
6
7
8
9
10
var invertTree = function (root) {
if (root === null) {
return null;
}
const left = invertTree(root.left);
const right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
};

5.5 给两颗二叉树,判断这两颗二叉树是否完全一样(100)

5.6 一颗二叉树,判断是否左右对称(101)

5.7 求完全二叉树的节点个数(222)

5.8 判断一颗二叉树是否是平衡二叉树(110)

5.9 判断二叉树和为 sum 的路径,必须包含子叶,必须到底(112)

方法一:递归终止条件,root==null 和 sum-nodeVal = 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const hasPathSum = (root, sum) => {
if (root == null) return sum == 0;
return (
hasPathSum(root.left, sum - root.val) ||
hasPathSum(root.right, sum - root.val)
);
};
// 修复bug-当直接传入null的时候,当第一个节点就等于sum值时
const hasPathSum = (root, sum) => {
if (root == null) return false; // 遍历到null节点, 只有首次传入null才会到这里
if (root.left == null && root.right == null) {
// 遍历到叶子节点,解决首个节点的值就和sum相等且其中一个子节点为null的情况
return sum - root.val == 0; // 如果满足这个就返回true
}
return (
hasPathSum(root.left, sum - root.val) ||
hasPathSum(root.right, sum - root.val)
); // 大问题转成两个子树的问题
};

5.10 求出二叉树所有左叶子和(404)

5.11 返回所有表示根节点到叶子节点路径的字符串(257)

递归,利用返回结果

终止条件:一般都是下边这个

1
2
3
4
if (root == null) return res; // 遍历到null节点, 只有首次传入null才会到这里
if (root.left == null && root.right == null) {
return ?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var binaryTreePaths = function (root) {
const paths = [];
const construct_paths = (root, path) => {
if (root) {
path += root.val.toString();
if (root.left === null && root.right === null) {
// 当前节点是叶子节点(叶子节点其实就是最底层的节点)
paths.push(path); // 把路径加入到答案中
} else {
path += '->'; // 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path);
construct_paths(root.right, path);
}
}
};
construct_paths(root, '');
return paths;
};

5.12 返回所有从根节点到叶子节点的路径,和为 sum(113II)

和上边一样就是字符串变为数组

5.13 每个节点只包含数字 0-9,求根节点到叶子节点的路径组装成一个数,所有路径的和(129)

1
2 3
就是 12+13=25

5.14 路径和为 sum,不一定为根节点开始,不一定叶子节点结束(437III)

思考:
node 分两种情况,在这个路径内,和不在这个路径内
pathSum 内 page 代表包含,sum1 和 sum2 为不包含的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var pathSum = function (root, sum) {
//root为根节点, sum为规定的路径权值和
if (!root) return 0; //若节点为空,返回0
let page = findDown(root, sum); //从根节点开始有多少满足条件的路径数,findDown函数是求从单个节点开始满足条件的路径数
let sum1 = pathSum(root.left, sum); //遍历左子树求符合条件的路径数,
let sum2 = pathSum(root.right, sum); //遍历右子树求符合条件的路径数
return page + sum1 + sum2; //得出总路径数
};

function findDown(tNode, sum) {
// 求从单个节点开始满足条件的路径数,tNode为当前节点,sum为规定的路径权值和
if (!tNode) return 0; //若节点为空,返回0
let flag = tNode.val === sum ? 1 : 0; // 当前节点权值刚好等于sum则算为1,否则为0
let leftSum = findDown(tNode.left, sum - tNode.val); //剩下的权值要子树来凑,先看左子树能不能凑出来
let rightSum = findDown(tNode.right, sum - tNode.val); //再看右子树是否能凑出来
return flag + leftSum + rightSum; // 返回符合条件的路径数
}

5.15 二分搜索树

插入、查找、删除
最大值、最小值
前驱、后继
上界、下界
某个元素的排名
寻找第 k 大元素

5.16 寻找二分搜索树两个节点的最近公共祖先(235)

如果一个小于等于 node 一个大于等于 node 则是公共祖先

1
2
3
4
5
6
7
8
9
const lowestCommonAncestor = (root, p, q) => {
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}
return root;
};

5.17 给二叉树,让验证是一颗二分搜索树(98)

5. 补1 将有序数组转换为二叉搜索树(108)

5.18 删除二分搜索树上其中一个节点(450)

5.19 将有序数组转化为一颗平衡二分搜索树(108)

5.20 给一个二分搜索树,求这颗树上第 k 小的元素(230)

5.21 找非二分搜索树的最近公共祖先 LCA(236)

6. 递归和回溯

回溯:结果往上返回到顶层-也是暴力写法的一种

排列问题–全排列
组合问题–组合总和

都转成树形问题

二维平面上使用回溯法

floodfill 算法 经典问题 深度优先遍历

6.1 手机键盘,数字字符串,给出所有字母组合(17)

输入:”23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]

可以先看成树形
a b c
def def def
树形都能递归解决

方法一:回溯
方法二:树形结构都能 dfs 和 bfs 进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//回溯
const letterCombinations = (digits) => {
if (digits.length == 0) return [];
const res = [];
const map = {
2: 'abc',
3: 'def',
4: 'ghi',
5: 'jkl',
6: 'mno',
7: 'pqrs',
8: 'tuv',
9: 'wxyz',
};

const dfs = (curStr, i) => {
// curStr是当前字符串,i是扫描的指针
if (i > digits.length - 1) {
// 指针越界,递归的出口
res.push(curStr); // 将解推入res
return; // 结束当前递归分支,进入另一个递归分支
}
const letters = map[digits[i]]; // 当前数字对应有哪些字母
for (const l of letters) {
// 不同的字母选择代表不同的递归分支
dfs(curStr + l, i + 1); // 生成新字符串,i指针右移,递归
}
};
dfs('', 0); // 递归的入口,初始字符串为'',指针为0
return res;
};

// dfs
var letterCombinations = function (digits) {
if (digits.length === 0) {
return [];
}

const dictionary = {
2: 'abc',
3: 'def',
4: 'ghi',
5: 'jkl',
6: 'mno',
7: 'pqrs',
8: 'tuv',
9: 'wxyz',
};

let result = [];

const dfs = (str, index) => {
// 当 index 等于 原始字符串时,说明当前分支已经组合完毕
// 可以结束递归了
if (index >= digits.length) {
result.push(str);
return;
}
// 获取当前数字对应的 字母集合
let map = dictionary[digits[index]];
// 处理 1 、0 等异常边界
if (map) {
// 当前字母集合中的每一个元素和上层递归中生成的更多的字符组合
for (let i of map) {
// 需要加入组合的位置向后移动
dfs(str + i, index + 1);
}
}
};
// 从字符串第一个位置开始组合
dfs('', 0);

return result;
};
// bfs
var letterCombinations = function (digits) {
if (digits.length === 0) {
return [];
}

const dictionary = {
2: 'abc',
3: 'def',
4: 'ghi',
5: 'jkl',
6: 'mno',
7: 'pqrs',
8: 'tuv',
9: 'wxyz',
};

let result = [];

// bfs
// 1、处理首位的 数值 为 1 或者 0 的情况
// 2、生成初始 的 组合基准值 方便使用while循环(手动初始化迭代栈)
if (digits[0] && digits[0] !== 1) {
result.push(...dictionary[digits[0]]);
}
let index = 1,
map,
temp;
while (index < digits.length) {
// 获取当前数字对应的 字母集合
map = dictionary[digits[index]];
if (map) {
// 用于记录 当前层的所有组合情况
temp = [];
for (let i = 0; i < map.length; i++) {
for (let j = 0; j < result.length; j++) {
// 以 前一个 数字 对应 组合条件为基准 生成的 组合状态
temp.push(result[j] + map[i]);
}
}
// 更新组合的状态
result = temp;
} else {
// 遇到 0、1 循环终止
break;
}
// 更新index,准备取下一个位置的数值
index++;
}

return result;
};

6.2 给数字加三个点,生成合法 ip(93)

输入:s = “25525511135”
输出:[“255.255.11.135”,”255.255.111.35”]


6.3 拆分字符串,使子串都是回文字符串(131)

输入:s=aab (一个字符也是回文字符串)
输出:[[‘aa’, b],[a,a,b]]

6.4 给一个数组,返回所有排列可能-全排列(46)

输入: [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const permute = (nums) => {
const res = [];
const used = {};

function dfs(path) {
if (path.length == nums.length) {
res.push(path.slice());
return;
}
for (const num of nums) {
// if (path.includes(num)) continue; // 查找的时间是O(n),别这么写,时间复杂度增加
if (used[num]) continue;
path.push(num);
used[num] = true;
dfs(path);
path.pop();
used[num] = false;
}
}

dfs([]);
return res;
};

6.5 给一个数组,返回所有排列可能(47)

输入: [1,1,2]
输出: [[1,1,2],[1,2,1],[2,1,1]]

6.6 给 n 个数,求 k 个数字的所有组合,不管顺序(77)

n=4 k=2
[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const combine = (n, k) => {
const res = [];

const helper = (start, path) => {
// start是枚举选择的起点 path是当前构建的路径(组合)
if (path.length == k) {
res.push(path.slice()); // 拷贝一份path,推入res
return; // 结束当前递归
}
for (let i = start; i <= n; i++) {
// 这里可以做一部剪纸的优化 i<=n-(k-path.length),因为后边不够k位了
// 枚举出所有选择
path.push(i); // 选择
helper(i + 1, path); // 向下继续选择
path.pop(); // 撤销选择
}
};

helper(1, []); // 递归的入口,从数字1开始选
return res;
};

6.7 给一个集合,从集合中取几个数(可使用多次)让和为 T,求这样的子集合(39)

输入:nums=[2,3,6,7] T=7
输出: [[7],[2,2,3]]

6.8 与上题不同,其中元素可以相同,一个元素只能使用一次(40)

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出: [[1, 7],[1, 2, 5],[2, 6],[1, 1, 6]]

6.9 1-9 中选出 k 个数字,每个数字只能使用一次,和为 n(216)

输入: k = 3, n = 7
输出: [[1,2,4]]

6.10 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)(78)

说明:解集不能包含重复的子集。
输入: nums = [1,2,3]
输出:
[[3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[]]

6.11 和上题一样,区别就是可以重复(90)

输入: [1,2,2]
输出:[[2],[1],[1,2,2],[2,2],[1,2],[]]

6.12 二进制手表,n 个灯是亮的,打印所有时间可能(401)

忽略

6.13 二维平面回溯法

6.14 给定一个二维网格和一个单词,找出该单词是否存在于网格中(79)

说明:
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
board =
[
[‘A’,’B’,’C’,’E’],
[‘S’,’F’,’C’,’S’],
[‘A’,’D’,’E’,’E’]
]
给定 word = “ABCCED”, 返回 true
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var exist = function (board, word) {
let l = word.length;
let l1 = board.length,
l2 = board[0].length;
if (l > l1 * l2) {
return false;
}
const directions = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let visited = new Array(l1).fill(0).map((e) => {
return Array(l2).fill(0);
});
for (let i = 0; i < l1; i++) {
for (let j = 0; j < l2; j++) {
let flag = check(i, j, 0);
if (flag) {
return true;
}
}
}
return false;

function check(x, y, k) {
if (word[k] != board[x][y]) {
return false;
}
if (k == l - 1) {
return true;
}
visited[x][y] = 1;
for (const [dx, dy] of directions) {
let newx = x + dx,
newy = y + dy;
if (newx >= 0 && newx < l1 && newy >= 0 && newy < l2) {
// 判断是否在区域内
if (!visited[newx][newy]) {
const flag = check(newx, newy, k + 1);
if (flag) {
return true;
}
}
}
}
visited[x][y] = 0;
return false;
}
};

6.15 floodfill 算法 经典问题 深度优先遍历

6.16 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。(200)

说明:岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
输入:grid = [
[“1”,”1”,”0”,”0”,”0”],
[“1”,”1”,”0”,”0”,”0”],
[“0”,”0”,”1”,”0”,”0”],
[“0”,”0”,”0”,”1”,”1”]
]
输出:3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var numIslands = function (grid) {
/** 解法一 DFS 关键:遇到1 则岛屿数量+1 且 将相邻所有陆地变为0 直至遍历完整个网格 */
// 边界处理
if (!grid.length) return 0;

// 定义变量
const row = grid.length,
col = grid[0].length; // row 行数 col 列数
let quantity = 0;

// DFS
const DFS = (grid, i, j) => {
// 递归终止条件
// 条件一 要在网格范围内
if (i < 0 || j < 0 || i >= row || j >= col) return;
// 条件二 当前值为 ‘1’
if (grid[i][j] !== '1') return;

// PS: 上述条件也可合并写 if (i < 0 || j < 0 || i >= row || j >= col || grid[i][j] !== '1') return 为了可读性 故拆开了

// 处理当前层逻辑 ⚠️ 是 '0' 不是 0
grid[i][j] = '0';

// 向 上下左右发散
for (let x of [-1, 0, 1]) {
for (let y of [-1, 0, 1]) {
// 过滤 对角线方向 (若为八个方向则移除此条件)
if (x === y || x === -y) continue;
DFS(grid, i + x, j + y);
}
}
};

// 遍历整个网格
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
if (grid[i][j] === '1') {
quantity++;
DFS(grid, i, j);
}
}
}

return quantity;
};

6.17 被包围(130)

给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充
示例:

X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:

X X X X
X X X X
X X X X
X O X X

6.18 回溯法是经典的人工智能的基础

6.19 N 皇后问题(51)+ 数独(37)

7 动态规划

递归是自上而下(从最大往回推)
动态规划是自下而上(从 0/1 起始开始往后)

递归问题 → 重叠子问题/(最优子结构) → 记忆化搜索(自顶向下)
→→→→→→→→→→→→→→→→→→→→ 动态规划 (自底向上)

7.1 斐波那切数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function fib(n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}

// 很多重复计算,存储优化
function main(n) {
var memo = new Array(n + 1).fill(-1);
function fib(n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (memo[n] == -1) {
memo[n] = fib(n - 1) + fib(n - 2);
}
return memo[n];
}
fib(n);
}

// 动态规划-自下而上 - 递归会占用空间,而动态规划不会
function fib(n) {
var memo = new Array(n + 1).fill(-1);
memo[0] = 0;
memo[1] = 1;
for (i = 2; i <= n; i++) {
memo[i] = memo[i - 1] + memo[i - 2];
}
return memo[n];
}

7.2 楼梯 n 节,可以走 1/2 节楼梯(70)

走 1 步,剩下爬 n-1 阶台阶
走 2 步,剩下爬 n-2 阶台阶

1
2
3
4
5
6
7
8
9
// 和菲波那切数列一样,需要存储优化
function climbStairs() {
function calcWay(n) {
if (n == 1) return 1;
if (n == 2) return 2; // 走两节有两种方法
return calcWay(n - 1) + calcWay(n - 2); // 走一步方法数+走两步的方法数
}
}
// 动态规划方法也相同

7.3 一个三角形数组,自底向上一条路径和最小(120)

7.4 m*n 的矩阵,从左上角向右下角走,和最小(64)

7.5 正整数 n,分割至少 2 个数,返回最大乘机(343)

10=(3+3+4) 乘机为 36

方法一:暴力解决
分割 4
4→1
→→2→1
→→3→2

分割 n
1….n-1

相当于从 n-1 一直到 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 递归
function intBreak(n) {
var memo = new Array(n - 1).fill(-1);
function breakInt(n) {
var res = -1;
if (n == 1) return 1;
if (memo[n] != -1) return memo[n];
for (i = 1; i < n - 1; i++) {
res = Math.max(res, i * breakInt(n - i), i * n - i); // 自顶向下
}
memo[n] = res;
return res;
}
}
function intBreak(n) {
var memo = new Array(n - 1).fill(-1);
// 从底开始设置
memo[1] = 1;
for (i = 2; i < n; i++) {
for (j = 1; j <= i - 1; j++) {
// 主要为了拆分为j+(i-j)
memo[i] = Math.max(memo[i], j * i - j, j * memo(i - j));
}
}
return memo[n];
}

7.6 给一个正整数,找最少完全平方数,使和为 n(279)

12 = 4+4+4
13=4+9

方法一:之前有用图轮处理过,参考 4.10

7.7 a-z 对应 1-26,给一个值,看有多少种方法可以解析这个数字字符串(91)

给 12 有 AB 和 L 两种情况
返回 2

7.8 机器人,m*n 矩阵,从左上向右下,只能右和下,有多少种走法(62)

7.9 和上题相同但是中间多了障碍物(63)

输入:obstacleGrid = [
[0,0,0],
[0,1,0],
[0,0,0]
]
输出:2

7.10 打劫问题(198)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

方法一:暴力解法 求出所有的组合
方法二:动态规划

偷取 0 考虑 2..n-1 → 再偷取 2 考虑 4..n-1/再偷取 3 考虑 5..n-1 …..
偷取 1 考虑 3..n-1 → 再偷取 3 考虑 5..n-1/再偷取 4 考虑 6..n-1 …..

状态定义:
考虑偷取[x…n-1]范围里的房子(函数的定义)
这里是考虑,并不是一定要偷

状态转移方程:
f(0) = max(v(0)+f(2),v(1)+f(3)….v(n-3)+f(n-1),v(n-2),v(n-1))

其实还可以定义其他状态:
考虑偷取[0,x]范围里的房子(函数的定义)
可以尝试做一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 迭代-记忆化搜索-自顶向下
function rob(nums) {
var memo = new Array(nums.length).fill(-1);
// 考虑抢劫nums[index....nums.length]
function tryRob(nums, index) {
// 递归终止条件
if (index > nums.length) return 0;
if (memo[index] != -1) return memo[index];
var res = 0;
for (i = index; i < nums.length; i++) {
res = Math.max(res, nums[index] + tryRob(index + 2));
}
memo[index] = res;
return res;
}
tryRob(nums, 0);
}
// 动态规划- 从右侧开始才是从底往上,所以从n-1开始,n-1是因为0也算一位
function rob(nums) {
var n = nums.length;
var memo = new Array(n).fill(-1);
// memo[i]表示考虑抢劫nums[i....n-1]
memo[n - 1] = nums[n - 1]; // 如果只抢劫最后一个,那就只有一种可能,就得到一个价值,就是nums[n-1]
for (i = n - 2; i >= 0; i--) {
for (j = i; j < n; j++) {
memo[i] = Math.max(memo[i], nums[j] + (j + 2 < n ? memo[j + 2] : 0));
}
}
return memo[0];
}

7.11 和上题相同,唯一不同是环形街道(213II)

7.12 和上题相同,唯一不同是二叉树形状的社区(337)

7.13 买卖股票问题(309)

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

7.14 0-1 背包问题

方法一:暴力解法,放进背包中,不放进背包中

参数个数:约束条件个数

F(n,C) n 个物品放入容量为 C 的最大价值
状态方程:
F(i,c) = Max( F(i-1, c), v(i) + F(i-1, c-w(i)))
Max(不放入+ 放入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 递归
function knapsack(w, v, C) {
var n = w.length;
var memo = new Array(n).fill(0).map(() => new Array(C + 1)); // 因为有两个维度,所以需要二维数组做缓存
function bestValue(w, v, index, c) {
if (index < 0 || c <= 0) return 0;
if (memo[index][c] != -1) return memo[index][c];
res = bestValue(w, v, index - 1, c); // 不放入
if (c >= w[index]) {
res = Math.max(res, v[index] + bestValue(w, v, index - 1, c - w[index]));
}
memo[index][c] = res;
return res;
}
bestValue(w, v, n - 1, C);
}
// 动态规划
function knapsack(w, v, C) {
var n = w.length;
var memo = new Array(n).fill(0).map(() => new Array(C + 1));
for (j = 0; j <= C; j++) {
memo[0][j] = j > w[0] ? v[0] : 0;
}
for (i = 1; i < n; i++) {
for (j = 0; j <= C; j++) {
memo[i][j] = memo[i - 1][j]; // 不放入所以还是i-1
if (j > w[i]) {
memo[i][j] = Math.max(memo[i][j], v[i] + memo[i - 1][j - w[i]]);
}
}
}
return memo[n - 1][C];
}

优化:
方法一:只能优化空间复杂度
现在是 n 行,其实可以只用 2 行就可以了,利用%来判断奇偶数,来互相覆盖
方法二:极致优化
可以优化到 1 行,因为只看左侧和上侧的值,所以可以从右侧开始处理

7.15 背包变种

7.16 完全背包-每个物品可以无限使用

但是容量是有限的,转化成有限问题

7.17 多重背包-每个物品有 num(i)个

类似完全背包

7.18 多维费用背包问题-考虑体积和重量,两个维度

多了一个状态,创建三位数组

1
2
3
4
var arr = [
[[], []],
[[], []],
];

7.19 约束-互相排斥,放入 A 就不能放入 B

7.20 约束-依赖,放入 A 就必须放入 B

7.21 背包实战-分割等和子集(416)

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

思路:
如果可以拆分,那必定含有一些元素相加后等于总和/2
这就是一个限制条件

转化为给 n 个物品,填满 c 的背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 迭代
function canPart(nums) {
var sum = 0;
for (i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 != 0) return false;
// -1为未计算,0为不可填充即false,1为可填充即true
// var memo = new Array(nums.length).fill(-1).map(() => new Array(sum/2+1));
tryPart(nums, nums.length - 1, sum / 2);
// nums[0...index]是否可以填满sum的背包
function tryPart(nums, index, sum) {
if (sum == 0) return true;
if (sum < 0 || index <= 0) return false;
// if(memo[index][sum] != -1) return memo[index][sum] = -1
// memo[index][sum] = (tryPart(nums, index-1, sum) || tryPart(nums, index-1, sum-nums[index]))?1:0
// return memo[index][sum] == 1
return (
tryPart(nums, index - 1, sum) ||
tryPart(nums, index - 1, sum - nums[index])
);
}
}
// 动态规划
function canPart(nums) {
var sum = 0;
for (i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 != 0) return false;
var n = nums.length;
var C = sum / 2;
var memo = new Array(C + 1).fill(false); // 从小向大,不会出现未计算的情况,节省一个空间复杂度
for (i = 0; i < C; i++) {
memo[i] = nums[0] == i; // 先拿出第一个看是否能填充
}
for (i = 1; i <= n; i++) {
for (j = C; j > nums[i]; j--) {
memo[j] = memo[j] || memo[j - nums[i]];
}
}
return memo[C];
}

7.22 零钱兑换(322)

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

7.23 组合总和 Ⅳ(377)

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。

7.24 单词拆分(139)

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

7.25 目标和(494)

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有 5 种方法让最终目标和为 3。

7.26 一和零(474)

7.28 最长子序列问题

7.29 最长递增子序列(300)

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

方法一:暴力解法-给出所有组合
方法二: 动态规划

LIS(i) 第 i 个数为结尾的最长上升子序列
转化为下边更好理解
LIS(i) 表示[0…i]的范围内,选择数字 nums[i]可以获得最长上升子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 动态规划  自底向下
function LIS(nums) {
if (nums.length == 0) return 0;
// memo[i] 表示以nums[i]为结尾的最长上升子序列
var memo = new Array(nums.length).fill(1);
for (var i = 1; i < nums.length; i++) {
// 因为第0位 取不取都是1种
for (var j = 0; j < i; j++) {
// 遍历之前的值做比较,判断是否小于当前值
if (nums[j] < nums[i]) {
memo[i] = Math.max(memo[j] + 1, memo[i]);
}
}
}
var res = 1;
for (var i = 0; i < nums.length; i++) {
res = Math.max(res, memo[i]);
}
return res;
}

7.30 摆动序列(376)

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

主要就是高低交替!

示例 1:

输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

7.31 最长公共子序列 LCS(1143)

像基因工程,相似度
S1 和 S2
S1=ABCD
S2=AEBD
公共子序列为 ABD

两个维度
LCS(m,n) S1[0..m]和 S2[0…n]的最长公共子序列的长度
其实和上一到题相同
方法一:递归思想 自顶向下
S1[m] == S2[n]
LCS(m,n) = 1+ LCS(m-1, n-1)
S1[m] != S2[n]
LCS(m,n) = Math.max(LCS(m, n-1), LCS(m-1, n))
方法二:自底向上

1
2


7.32 dijktra 单元最短路径算法

之前用图论解的
也可以尝试用动态规划
shortPath(i) 为从 start 到 i 的最短路径长度
shortPath(x) = min(shortPath(a)+w(a→x))

7.33 求完全解

即不仅仅查找到最大值,也要打印出相应的值或者数组

解法思路

1.1 双索引

1.1.1 对撞指针

1、回文串问题
2、数组翻转问题 –设置一个缓存变量来,交换左右两个位置
3、有序数组两数字和为 target(167)
4、求两面墙容积最大(11)

1.1.2 滑动窗口

while
1、有序数组中连续子数组求和(209)
2、查找不汉重复字符的子串(3)V-3-8
3、找出包含所有子串字符的索引(438) V-3-8
4、链表(19) v-5-4

2.1 多层遍历

1、4 层拆成 2 个 2 层

3.1 Map 和 Set

1、3sum 4sum 的值,充分利用 target-a
2、相同字符串,可以颠倒顺序
4、查找边和点的次数个数问题

4.1 补虚拟头节点+node1+node2+next 节点

next 可以不要
1、链表交换

4.2 遍历一遍找到链表中间值

快慢指针-基本思想:
设置两个指针 p,q,初始时,分别指向头结点,循环使 p 后移两位,q 后移一位,当单链表遍历完毕时,q 的位置就是中间位置。

5.1 最短路径

树/图论/动态规划

其他算法

1.1 旋转图像

首页将输入
1 2 3
4 5 6
7 8 9
通过交换 matrix[i][j], matrix[j][i] 得到
1 4 7
2 5 8
3 6 9
最后将得到每组数组倒序排列即可
7 4 1
8 5 2
9 6 3

1
2
3
4
5
6
7
8
9
10
11
var rotate = function (matrix) {
let matrixLength = matrix.length;
for (let i = 0; i < matrixLength; i++) {
for (let j = i; j < matrixLength; j++) {
let temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
return matrix.map((item) => item.reverse());
};

参考资料

1.2 螺旋矩阵(54)

← Prev Next →