设计链表

设计链表

https://leetcode.cn/problems/design-linked-list/description/

单链表

对链表的操作有两种方式:https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html#%E6%80%9D%E8%B7%AF

  • 直接使用原来的链表进行操作
  • 设置一个虚拟头结点进行操作

按照索引添加和删除结点要注意遍历得到关键结点应该是index前面的结点
根据索引get对应元素时,遍历得到的关键结点应该是index这个结点

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

var MyLinkedList = function () {
this.val = null
this.next = null
}

/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function (index) {
let cur = this
for (let i = 0; i <= index; i++) {
if (cur.next) {
cur = cur.next
} else {
return -1
}
}
return cur.val
}

/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
const newNode = new MyLinkedList()
newNode.val = val
newNode.next = this.next
// 从这一行可以清楚的看出,这是添加虚拟节点的操作方式
this.next = newNode
}

/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
const newNode = new MyLinkedList()
newNode.val = val
newNode.next = null
let cur = this
while (cur.next) {
cur = cur.next
}
cur.next = newNode
}

/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
const newNode = new MyLinkedList()
newNode.val = val
let cur = this
for (let i = 0; i < index; i++) {
if (cur.next) {
cur = cur.next
} else {
return
}
}
newNode.next = cur.next
cur.next = newNode
}

/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function (index) {
let cur = this
for (let i = 0; i < index; i++) {
if (cur.next) {
cur = cur.next
} else {
return
}
}
if (!cur.next) return
cur.next = cur.next.next
}

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

双链表

双链表多了一个prev要处理

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
var MyLinkedList = function () {
this.prev = null
this.val = null
this.next = null
}


MyLinkedList.prototype.get = function (index) {
let cur = this
for (let i = 0; i <= index; i++) {
if (cur.next) {
cur = cur.next
} else {
return -1
}
}
return cur.val
}

/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
const newNode = new MyLinkedList()
newNode.val = val
newNode.next = this.next
newNode.prev = this
this.next = newNode
}

/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
const newNode = new MyLinkedList()
newNode.val = val
newNode.next = null
let cur = this
while (cur.next) {
cur = cur.next
}
newNode.prev = cur
cur.next = newNode
}

/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
const newNode = new MyLinkedList()
newNode.val = val
let cur = this
for (let i = 0; i < index; i++) {
if (cur.next) {
cur = cur.next
} else {
return
}
}
newNode.next = cur.next
newNode.prev = cur
cur.next = newNode
}

/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function (index) {
let cur = this
for (let i = 0; i < index; i++) {
if (cur.next) {
cur = cur.next
} else {
return
}
}
if (!cur.next) return
cur.next.prev = cur
cur.next = cur.next.next
}

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

有序数组的平方

有序数组的平方

图 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var sortedSquares = function(nums) {
// 初始化两个指针
let left =0;
let right = nums.length - 1;
// 初始化存放结果的数组
let res = new Array(nums.length);
// 从后往前依次填满res
let point = right
while(left <= right){
if(nums[left]**2 > nums[right]**2){
res[point] = nums[left]**2;
left++;
}
else{
res[point] = nums[right]**2;
right--;
}
point--
}
return res
};

长度最小的有序数组

长度最小的有序数组

https://leetcode.cn/problems/minimum-size-subarray-sum/description/

图 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var minSubArrayLen = function(target, nums) {
//初始化
const n = nums.length;
let ans = Number.MAX_SAFE_INTEGER;
let left = 0; right = 0;
// 核心部分
while(right<n){ //终止条件
target -= nums[right];
while(target <= 0){ // 满足条件的情况
ans = Math.min(ans, right-left+1);
target += nums[left];
left++;
}
right++
}
return ans === Number.MAX_SAFE_INTEGER ? 0 : ans;
};

移除元素

移除元素

https://leetcode.cn/problems/remove-element/description/

单指针法

图 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var removeElement = function (nums, val) {
// 单指针
let size = nums.length
let i = 0
while (i < size) {
if (nums[i] === val) {
for (let j = i; j < size; j++) {
nums[j] = nums[j + 1]
}
size--
} else {
i++
}
}
return size
}
由于题目没有要求顺序问题,所以不一定要和上面一样,把所有数字往前移。 核心逻辑,发现和目标值一样的数,用数组的最后一位代替当前数,然后数组size--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var removeElement = function(nums, val) {
// 单指针
let size = nums.length
let i=0
while(i<size){
if (nums[i] === val){
nums[i] = nums[size-1]
size--
}
else{
i++
}
}
return size
};

双指针法

图 1

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
var removeElement = function (nums, val) {
// 双指针法
let slow =0;
let fast = 0;
while(fast < nums.length){
if(nums[fast] !== val){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}

螺旋矩阵II

螺旋矩阵II

https://leetcode.cn/problems/spiral-matrix-ii/description/

循环不变量

每次循环处理的区间是有规律的
左闭右开

1
2
3
4
5
6
o . . . 1 o
1 . . . . .
. . . . . .
. . . . . .
. . . . . 1
o 1 . . . o

解题

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
var generateMatrix = function(n) {
// 初始化行列位置
let startx = 0, starty = 0;
let end = n-1;
count = 1;
let matrix = new Array(n).fill(0).map(()=>new Array(n).fill(0));
while(Math.floor(n/2)){
// 这里面的四个for循环戴白哦这上节示意图中的一个o-1的过程
// 要想通这里的控制过程,并且知道startx,starty,和end的意思,以及他们在每一次循环中如何变化,n如何变化。
for (let j=starty;j<end;j++){
matrix[startx][j] = count++;
}
for(let i = startx;i<end;i++){
matrix[i][end] = count++;
}
for(let j=end;j>starty;j--){
matrix[end][j] = count++;
}
for(let i=end;i>startx;i--){
matrix[i][starty] = count++;
}
end--;
startx++;
starty++;
n-=2;
}
//应对n为奇数的情况
if (n) {
matrix[startx][starty] = count;
}
return matrix;
};

回文字符串

回文字符串

什么是回文字符串

回文字符串:是一个正读和反读都一样的字符串。

判断回文字符串的方法

双指针法

这是最直观的方法。我们可以设置两个指针,一个从字符串的开始位置开始,一个从字符串的结束位置开始,然后同时向中间移动。在移动的过程中,如果两个指针指向的字符不相等,那么这个字符串就不是回文字符串。如果两个指针都移动到了中间位置,那么这个字符串就是回文字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let str = 'abba';
let str2 = 'abdc';

function isPalindrome(s) {
let left = 0,right = s.length-1
while(left<right){
if(s[left] != s[right]){
return false
}
left++
right--
}
return true
}

console.log(isPalindrome(str)) // true
console.log(isPalindrome(str2)) // false

反转字符串

我们可以将字符串反转,然后比较反转后的字符串和原字符串是否相等。如果相等,那么这个字符串就是回文字符串。

1
2
3
4
5
6
7
8
9
10
let str = 'abba';
let str2 = 'abdc';

function isPalindrome(s) {
// 反转字符串
let reverseStr = s.split('').reverse().join('')
return s === reverseStr
}
console.log(isPalindrome(str)) // true
console.log(isPalindrome(str2)) // false

递归

我们可以递归地判断一个字符串是否为回文字符串。如果字符串的首字符和尾字符相等,并且去掉首字符和尾字符后的字符串仍然是回文字符串,那么这个字符串就是回文字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let str = 'abba';
let str2 = 'abdc';

function isPalindrome(s) {
if (s.length <= 1) {
return true;
}
if (s[0] !== s[s.length - 1]) {
return false;
}
return isPalindrome(s.slice(1, s.length - 1));
}

console.log(isPalindrome(str)) // true
console.log(isPalindrome(str2)) // false

滑动窗口

滑动窗口

算法题应用场景

满足***条件的(计算结果、出现次数),包含最长/最短 子串/子数组/子序列

滑动窗口的思路

图 0

滑动窗口模板

1
2
3
4
5
6
7
8
9
10
// 找最长的模板
// 初始化left,right,result,bestresult
while(右指针到达结尾处){
窗口逐渐扩大,加入right对应元素,更新result
while(result不满足要求){
窗口缩小、移除left对应元素,left右移
}
更新最优结果bestresult
right++
}
1
2
3
4
5
6
7
8
9
10
// 找最短模板
// 初始化left,right,result,bestresult
while(右指针到达结尾处){
窗口扩大,加入right对应元素,更新result
while(result满足条件){
更新最优结果bestresult
窗口缩小,移除left对应元素,left右移
}
right++
}

题目

无重复字符串的子串

https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/

重点在于如何判断无重复字符串

判断是否有重复字符串,使用字典map和调整left位置实现无重复子串。

图 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var lengthOfLongestSubstring = function(s) {
// 初始化
let p=0;q=0;max=0;
let map = new Map();
// 右指针逐渐向结尾处移动
while(q<s.length){
// 当结果不满足条件
if(map.has(s[q])){
p = Math.max(map.get(s[q])+1,p)
}
map.set(s[q],q);
max = Math.max(max,q-p+1);
q++;
}
}

串联所有单词的子串

https://leetcode.cn/problems/substring-with-concatenation-of-all-words/description/

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
var findSubstring = function(s, words) {
let res = []
let wordLen = words[0].length
let wordNum = words.length
let wordMap = new Map()
for (let word of words) {
wordMap.set(word, wordMap.has(word) ? wordMap.get(word) + 1 : 1)
}
// 这里i的设置比较难理解,但是如果理解了后面while中的逻辑,就懂了
// 从i开始,right直接到最后可能的位置,也就是for中的一次遍历可以找到0或多个res,而不是1个
for (let i = 0; i < wordLen; i++){
let left = i
let right = i
let window = new Map()
let count = 0
while (right + wordLen <= s.length) {
let w = s.slice(right, right + wordLen)
right += wordLen
if (!wordMap.has(w)) {
window.clear()
count = 0
left = right
} else {
window.set(w, window.has(w) ? window.get(w) + 1 : 1)
count++
while (window.get(w) > wordMap.get(w)) {
let l = s.slice(left, left + wordLen)
left += wordLen
window.set(l, window.get(l) - 1)
count--
}
if (count === wordNum) {
res.push(left)
}
}
}
}
return res
}
Text
1
2
3
4
5
6
7
8
9
10
11
12
13
这段代码的主要逻辑是使用滑动窗口来遍历字符串 s,并检查每个窗口是否包含 words 中的所有单词。

1、let left = i 和 let right = i:初始化滑动窗口的左右边界。
2、let window = new Map():创建一个哈希映射 window 来存储当前窗口中每个单词出现的次。
3、let count = 0:初始化一个计数器 count 来存储当前窗口中的单词总数。
4、while (right + wordLen <= s.length):当窗口的右边界加上单词的长度不超过字符串 s 的长度时,继续循环。
5、let w = s.slice(right, right + wordLen):获取窗口右边界的单词 w。
6、right += wordLen:将窗口的右边界向右移动一个单词的长度。
7、if (!wordMap.has(w)):如果 w 不在 wordMap 中,那么清空 window,重置 count,并将窗口的左边界移动到 right。
8、else:如果 w 在 wordMap 中,那么更新 window 和 count,并检查 window 中 w 的数量是否超过了 wordMap 中 w 的数量。如果超过了,那么从窗口的左边移除单词,直到 window 中 w 的数量不超过 wordMap 中 w 的数量。
9、if (count === wordNum):如果 count 等于 wordNum,那么将窗口的左边界添加到结果数组 res 中。

这个滑动窗口的策略确保了我们能够有效地遍历字符串 s,并找出所有包含 words 中所有单词的子串的起始索引。

两数相加

链表的定义和分类

单链表

图 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
自定义链表节点
function ListNode(val,next){
this.val = (val===undifined ? 0 : val)
this.next = (next === undefined ? 0 : val)
}

class ListNode{
val;
next = null;
constructor(value){
this.val = value;
this.next = null
}
}
1
2
3
4
5
6
7
8
class ListNode{
public val : number;
public next : ListNode|null = null
constructor(value:number){
this.val = value;
this.next = null;
}
}

双链表

图 1

循环链表

图 2

链表相关题目

两数相加

https://leetcode.cn/problems/add-two-numbers/description/

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
var addTwoNumbers = function(l1, l2) {
// 首先新建一个节点
let res = new ListNode(0)
// 将节点赋值给变量curr,最终返回res.next就是链表的头,
// p和q分别是第一个和第二个链表的头
let curr = res, p = l1, q = l2
//进位的时候需要的carry
let carry = 0
//循环终止的条件是两个链表元素都被遍历完
while(p != null || q != null){
// 将长度较短的链表后面补0,直到达到较长链表的长度
let x = (p === null) ? 0 : p.val
let y = (q === null) ? 0 : q.val
// 求对应位置两数的和
let sum = x + y + carry
// 和大于0说明需要进位,进位数为sum/2
carry = Math.floor(sum/10)
// 保存在链表中的应该是余数,且应该创建一个ListNode
curr.next = new ListNode(sum%10)
// 让curr指向curr.next
curr = curr.next
// p和q都指向下一个结点
if (p != null) p = p.next
if (q != null) q = q.next
}
// 最后有可能出现两数的和的长度大于两数最长的哪个数的位数
if (carry > 0){
curr.next = new ListNode(carry)
}
return res.next
};

Array.form

Array.form用法

Array.from() 是一个静态方法,它从类数组或可迭代对象创建一个新的数组实例。
以下是一些使用 Array.from() 的例子:

从字符串创建数组:

1
2
3
let str = 'hello';
let arr = Array.from(str);
console.log(arr); // 输出:['h', 'e', 'l', 'l', 'o']

从 Set 创建数组:

1
2
3
let set = new Set(['a', 'b', 'c']);
let arr = Array.from(set);
console.log(arr); // 输出:['a', 'b', 'c']

从 Map 创建数组:

1
2
3
let map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
let arr = Array.from(map);
console.log(arr); // 输出:[[1, 'a'], [2, 'b'], [3, 'c']]

从类数组对象创建数组:

1
2
3
let obj = {0: 'a', 1: 'b', 2: 'c', length: 3};
let arr = Array.from(obj);
console.log(arr); // 输出:['a', 'b', 'c']

使用映射函数:

1
2
let arr = Array.from([1, 2, 3], x => x * x);
console.log(arr); // 输出:[1, 4, 9]

在你的代码中,Array.from({length:n},()=>Array.from({length:n},()=>’.’)) 创建了一个 n x n 的二维数组,所有元素都被初始化为 ‘.’。

回溯算法

回溯算法

回溯算法基本理论

是什么:

回溯采用是错得方法解决问题,一旦发现当前步骤失败,回溯算法就会返回上一个步骤

解决哪些问题:

  • 组合问题(组合不强调顺序,而排列强调)
  • 切割问题
  • 子集问题
  • 排列问题
  • 棋盘问题

如何理解回溯法

回溯法都可以抽象为一个树形结构。一个n叉树

图 0

回溯法的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 递归函数没有返回值
void backtrading(参数){
// 递归终止条件
if (终止条件){ //通常在叶子节点
//收集结果
return
}
// 单层搜索
for (集合得元素集){ //遍历集合中的每一个元素
// 处理节点
// 递归函数
// 回溯
}
return
}

回溯算法剪枝

剪枝一般都是在for循环的i的范围上做文章,例如下面的组合题目,确定i至多从哪里开始。
图 4

回溯算法题目

组合问题

题目:https://leetcode.cn/problems/combinations/description/

图 2

未剪枝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var combine = function(n, k) {
var path = []; // 用来存放每一个结果
var res = []; //存放所有结果的数组
//确定参数
const backTrading = (n, k, startIndex) => { //startIndex搜索的起始位置
//终止条件
if(path.length === k){
//注意这里要深拷贝一下path,因为path是引用类型,后续的操作会改变path的值
res.push([...path]); //将path的值push到res中
return;
}
// 单层递归逻辑
for(let i = startIndex; i <= n; i++){
path.push(i);
backTrading(n, k, i + 1); //递归,起始位置+1
path.pop(); //回溯的过程,弹出最后一个元素
}
}
backTrading(n, k, 1); //调用backTrading函数,第一次竟然忘了这一步
return res;
};

剪枝

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
// path.length 已选取的元素的大小
// k-path.length 还需要的元素
// n-(k-path.length)+1 i的至多从该位置开始取
var combine = function(n, k) {
var path = []; // 用来存放每一个结果
var res = []; //存放所有结果的数组
//确定参数
const backTrading = (n, k, startIndex) => { //startIndex搜索的起始位置
//终止条件
if(path.length === k){
//注意这里要深拷贝一下path,因为path是引用类型,后续的操作会改变path的值
res.push([...path]); //将path的值push到res中
return;
}
// 单层递归逻辑
//剪枝在for循环停止的位置做文章
for(let i = startIndex; i <= (n-(k-path.length)+1); i++){
path.push(i);
backTrading(n, k, i + 1); //递归,起始位置+1
path.pop(); //回溯的过程,弹出最后一个元素
}
}
backTrading(n, k, 1); //调用backTrading函数,第一次竟然忘了这一步
return res;
};

组合总和 III

https://leetcode.cn/problems/combination-sum-iii/description/

图 5

图 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//加了剪枝操作
var combinationSum3 = function(k, n) {
let path = [] //一维数组,用来存放符合条件的组合
let res = [] //二维数组,用来存放符合条件的组合数组
const backTrading = (n,k,sum,startIndex)=>{ //sum是当前path数组的和
if(sum>n) return //这是一个剪枝操作
if(path.length ===k){
if(sum === n){
res.push([...path])
}
return
}
// i的边界的控制也是剪枝操作
for(let i = startIndex;i<=(9-(k-path.length)+1);i++){
sum += i
path.push(i)
backTrading(n,k,sum,i+1) //这里是深入分支
sum -= i //回溯,这里是广度的遍历,挨个遍历每一个元素
path.pop()
}
}
backTrading(n,k,0,1)
return res
};

电话号码的字母组合

https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/

解题思路

  • 先做数字和字母的映射
  • 通过树形结构梳理思路

图 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
27
28
29
30
31
var letterCombinations = function(digits) {
//先做一个映射表
const map = new Map()
map.set('2','abc')
map.set('3','def')
map.set('4','ghi')
map.set('5','jkl')
map.set('6','mno')
map.set('7','pqrs')
map.set('8','tuv')
map.set('9','wxyz')
let str = '' //收获单个结果
let res = []
const backTrading = (digits,index)=>{
if(!digits) return
if(index === digits.length){ //终止条件
res.push(str)
return
}
//单层逻辑
let digit = digits[index] //找到数字
let letters = map.get(digit) // 根据数字在映射表中找到对应的字母
for(let i=0;i<letters.length;i++){
str += letters[i]
backTrading(digits,index+1)
str = str.slice(0,str.length-1) //回溯
}
}
backTrading(digits,0)
return res
};

组合总和

https://leetcode.cn/problems/combination-sum/description/

数组中没有0且没有重复数字。

图 8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var combinationSum = function(candidates, target) {
//递归终止条件
let path = []
let res = []
const backTraking = (candidates,target,sum,startIndex)=>{
if (sum > target) return
if (sum === target) {
res.push(path.slice()) //深拷贝
return
}
for(let i = startIndex;i<candidates.length;i++){
path.push(candidates[i])
sum += candidates[i]
backTraking(candidates,target,sum,i) //可以重复i不加1喽
sum -= candidates[i]
path.pop()
}
}
backTraking(candidates,target,0,0)
return res
};

剪枝
图 9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var combinationSum = function(candidates, target) {
//递归终止条件
let path = []
let res = []
const backTraking = (candidates,target,sum,startIndex)=>{
// if (sum > target) return
if (sum === target) {
res.push(path.slice()) //深拷贝
return
}
for(let i = startIndex;i<candidates.length;i++){
if (candidates[i] + sum > target) continue; //剪枝
path.push(candidates[i])
sum += candidates[i]
backTraking(candidates,target,sum,i) //可以重复i不加1喽
sum -= candidates[i]
path.pop()
}
}
backTraking(candidates,target,0,0)
return res
};

组合总和 II

https://leetcode.cn/problems/combination-sum-ii/description/

难点在于:结果中不能有重复的结果[1,7],[7,1]。但是能原数组中有两个1,有可以有[1,1,6]。

图 10

树层去重树枝去重

https://www.programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html#%E6%80%9D%E8%B7%AF

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
var combinationSum2 = function(candidates, target) {
let path = []
let res = []
// 进行排序
candidates.sort((a,b)=>a-b)
const backTracking = (candidates,target,sum,startIndex,used)=>{
if (sum === target){
res.push(path.slice())
return
}
for(let i = startIndex;i<candidates.length;i++){
if (sum + candidates[i] > target) continue //剪枝
// i>0是为了保证i-1有意义
// used[i-1]=false,说明此时处于树层的状态,即同一树层上的前一个元素已经被撤销,所以此时可以继续使用
if (i > 0 && candidates[i] === candidates[i-1] && !used[i-1]) continue //数层去重的而过程,前提是数组有序
path.push(candidates[i])
sum += candidates[i]
used[i] = true //标记使用过
backTracking(candidates,target,sum,i+1,used)
//回溯
sum -= candidates[i]
path.pop()
used[i] = false
}
}
backTracking(candidates,target,0,0,new Array(candidates.length).fill(false))
return res
};

分割回文子串

https://leetcode.cn/problems/palindrome-partitioning/description/

图 11

难点:

  • 切割问题和组合是一样的,之前选择的元素下标,相当于这里的切割线
  • 如何表示切割的字串[startIndex,i]
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
var partition = function(s) {
let path = [] //一维数组,用来存放每一次的结果
let res = [] //二维数组,用来存放所有的结果、
const backTraking = (s,startIndex)=>{
if(startIndex >= s.length){ //到了叶子就节点了,将结果放入res中
res.push(path.slice())
return
}
// 单层搜索逻辑
for(let i=startIndex;i<s.length;i++){
// 如果是回文子串,才添加到path中,否则直接continue
if(isPlalindrome(s,startIndex,i)){
path.push(s.substring(startIndex,i+1))
backTraking(s,i+1)
path.pop()
}
}
}
const isPlalindrome = (s,left,right)=>{
//判断是否是回文串的函数
while(left<right){
if(s[left] !== s[right]) return false
left++
right--
}
return true
}
backTraking(s,0)
return res
};

复原IP地址

https://leetcode.cn/problems/restore-ip-addresses/description/

合法的IP地址:有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

图 12

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 restoreIpAddresses = function(s) {
let path = []
let res = []
const backTracking = (s,startIndex)=>{
if(path.length === 4 && startIndex === s.length){
res.push(path.join('.'))
return
}
//剪枝了,如果path的长度已经是4了,但是startIndex还没有到s的长度,那么就不用再继续了
if(path.length === 4 && startIndex < s.length) return
// 剪枝了,如果path的长度不是4了,但是startIndex已经到s的长度了,那么也不用再继续了
for(let i= startIndex;i<s.length && i<= startIndex+3;i++){
if (isValid(s,startIndex,i)){
path.push(s.substring(startIndex,i+1))
backTracking(s,i+1)
path.pop()
}
}
}
const isValid = (s,startIndex,i)=>{
if(s[startIndex] === '0' && i !== startIndex) return false
if(parseInt(s.substring(startIndex,i+1)) > 255) return false
return true
}
backTracking(s,0)
return res
};
前面的一系列都是在叶子节点收获结果,终止条件的写法基本一致

后面开始要从每一个节点收获结果,终止条件的写法发生变化。

子集

https://leetcode.cn/problems/subsets/description/

图 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var subsets = function(nums) {
let path = []
let res = []
const backTraking = (nums,startIndex)=>{
// 每个节点都收获一次、如果终止条件改为>,这一句要放在if的下面,单层搜索逻辑的上面。
res.push([...path])
//终止条件是,到了叶子节点
if(startIndex >= nums.length){
return //剩余元素为空时,也就是startIndex到达了nums的长度
}

for(let i=startIndex;i<nums.length;i++){
path.push(nums[i])
backTraking(nums,i+1)
path.pop()
}
}
backTraking(nums,0)
return res
};

子集II

树层去重:确定当前在树层used[i-1]===fasle,当前元素与前一元素相等,i>0

https://leetcode.cn/problems/subsets-ii/description/

图 14

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
var subsetsWithDup = function(nums) {
let path = []
let res = []
used = new Array(nums.length).fill(false)
// 排序
nums.sort((a, b) => a - b)
const backTraking = (nums, startIndex, used) => {
res.push([...path])
if (startIndex >= nums.length) {
return
}
for (let i = startIndex; i < nums.length; i++) {
// i>0是为了保证i-1有意义
// used[i-1]=false,说明此时处于树层的状态,即同一树层上的前一个元素
// 已经被撤销,所以此时可以继续使用
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) {
continue
}
path.push(nums[i])
used[i] = true
backTraking(nums, i + 1, used)
path.pop()
used[i] = false
}
}
backTraking(nums, 0, used)
return res
};

递增子序列

https://leetcode.cn/problems/non-decreasing-subsequences/

去重方法:由于不能排序,所以这里的树层去重和之前有所差别
之前的题目基本使用used来记录事都使用过某一元素,因为是有序的,只需判断used[i-1] ,nums[i-1]===nums[i]
现在得到题目是无序的,使用set,每次循环判断当前数字是否在set中,如果在,跳过,否则将数字放入set,实现去重。

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
var findSubsequences = function(nums) {
let path = []
let res = []
// 回溯算法函数
const backTraking = (nums,startIndex)=>{
// 收获结果的地方
if (path.length > 1){
res.push([...path])
}
// 终止条件
if (startIndex >= nums.length){
return
}
//单层搜索逻辑
let set = new Set()
for(let i=startIndex;i<nums.length;i++){
// 当前元素小于path最后的元素,跳过. 但是如果path为空,不跳过
if (nums[i] < path[path.length-1] || !path ){
continue
}
// 树层去重
if(set.has(nums[i])){
continue
}
else{
set.add(nums[i])
}
path.push(nums[i])
backTraking(nums,i+1)
path.pop()
}
}
backTraking(nums,0)
return res
};

全排列

https://leetcode.cn/problems/permutations/description/

首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
图 15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var permute = function(nums) {
let path =[]
let res = []
let used = new Array(nums.length).fill(false)
const backTracking = (nums)=>{
if(path.length === nums.length){
res.push([...path])
return
}
for(let i=0; i<nums.length; i++){
if(used[i]){
continue
}
used[i] = true
path.push(nums[i])
backTracking(nums)
path.pop()
used[i] = false
}
}
backTracking(nums)
return res
};

全排列II

https://leetcode.cn/problems/permutations-ii/description/

加一个数层去重

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
var permuteUnique = function(nums) {
let path = []
let res = []
// 用于排列
let used = new Array(nums.length).fill(false)
// 用于数层去重
let used1 = new Array(nums.length).fill(false)
nums.sort((a,b)=>a-b)
const backTracking = (nums)=>{
// 终止条件
if(path.length === nums.length){
res.push([...path])
return
}
for(let i=0; i<nums.length; i++){
if (used[i]){
continue
}
//树层去重
if(i>0 && nums[i] === nums[i-1] && !used1[i-1]){
continue
}
used[i] = true
used1[i] = true
path.push(nums[i])
backTracking(nums)
path.pop()
used[i] = false
used1[i] = false
}
}
backTracking(nums)
return res
};

重新安排行程

https://leetcode.cn/problems/reconstruct-itinerary/description/

N皇后

https://leetcode.cn/problems/n-queens/

图 16

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
var solveNQueens = function(n) {
let res = []
// Array.from() 是一个静态方法,它从类数组或可迭代对象创建一个新的数组实例。
let checkboard = Array.from({length:n},()=>Array.from({length:n},()=>'.'))
const isVaild = (row,col,checkboard,n)=>{
// 检查列,只需要检查当前行上面的行
for(let i=0;i<row;i++){
if(checkboard[i][col] === 'Q'){
return false
}
}
// 检查左上方45度对角线
for(let i= row-1,j=col-1;i>=0 && j>=0;i--,j--){
if(checkboard[i][j] === 'Q'){
return false
}
}
// 检查右上方45度对角线
for(let i=row-1,j=col+1;i>=0 && j<n;i--,j++){
if(checkboard[i][j] === 'Q'){
return false
}
}
return true
}

const backTraking = (checkboard,n,row)=>{
if(row==n){
res.push(checkboard.map(row=>row.join('')))
return
}
for (let col=0;col<n;col++){
//判断放在这里是否符合要求
if (isVaild(row,col,checkboard,n)){
checkboard[row][col] = 'Q'
backTraking(checkboard,n,row+1)
checkboard[row][col] = '.'
}
}
}
backTraking(checkboard,n,0)
return res
};

解数独

https://leetcode.cn/problems/sudoku-solver/
https://www.programmercarl.com/0037.%E8%A7%A3%E6%95%B0%E7%8B%AC.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE

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 solveSudoku = function (board) {
function isValid(row, col, val, board) {
let len = board.length
// 行不能重复
for (let i = 0; i < len; i++) {
if (board[row][i] === val) {
return false
}
}
// 列不能重复
for (let i = 0; i < len; i++) {
if (board[i][col] === val) {
return false
}
}
// 3*3的格子不能重复
let startRow = Math.floor(row / 3) * 3 // 0,3,6
let startCol = Math.floor(col / 3) * 3 // 0,3,6

for (let i = startRow; i < startRow + 3; i++) {
for (let j = startCol; j < startCol + 3; j++) {
if (board[i][j] === val) {
return false
}
}
}
return true
}

function backTracking() {
for (let i = 0; i < board.length; i++) {
for (let j = 0; j < board[0].length; j++) {
if (board[i][j] !== '.') continue
for (let val = 1; val <= 9; val++) {
if (isValid(i, j, `${val}`, board)) {
board[i][j] = `${val}`
if (backTracking()) {
return true
}

board[i][j] = `.`
}
}
return false
}
}
return true
}
backTracking(board)
return board
};