回溯法是一种暴力搜索方法,往前一步探索,如果发现该选择不符合,那么就退回一步重新选择。
46.全排列:used,背诵思路!
- 排列问题是有序的,也就是说 [1, 2] 和 [2, 1] 是两个集合,数字 1 使用了两次,因此需要一个 used 数组来标记已经选择的元素。
-
递归终止条件:当收集元素的数组 path 的长度等于题目中给出的 nums 数组长度时,说明找到了一个全排列,递归到了叶子节点,此时递归终止。
-
单层搜索逻辑:used 数组就是记录此时 path 里都有哪些元素使用了,一个排列里一个元素只能使用一次。
「注意」 由于 path 中存的是地址引用,如果直接传递 path ,后续操作 path 时前面存放的 path 内容也会被改变,不符合我们的预期。因此,可以用 path.slice()进行浅拷贝。
const permute = function(nums) {
const res = [];
const path = []; // 存储一个排列
const dfs = function (arr, used) {
if (path.length == arr.length) { // 找到一个全排列,递归应该终止
res.push(path.slice());
return; // 注意
}
for (let i = 0; i < arr.length; i++) {
if(used[i]) continue; // 每个元素只能在path里填充一次
path.push(arr[i]);
used[i] = true;
dfs(arr, used); // 递归直到找到一个全排列,此时 if 会 return
path.pop(); // 回溯,继续下一个排列
used[i] = false;
}
}
dfs(nums, []);
return res;
}
时间复杂度:O(n * n!)。
空间复杂度:O(n)。
22. 括号生成:回溯
这题的思路就是要一直选括号,要么左括号,要么右括号。有 2 个约束:
- 对于左括号,只要 '(' 有剩余,就可以选。
- 对于右括号,当剩下的右括号比左括号多时,才可以选右括号。
const generateParenthesis = function(n) {
const res = [];
const backTrack = function(left, right, str) { // 左右括号剩余的数量,str 是当前正在构建的字符串
if (str.length == 2 * n) { // 递归终止条件
res.push(str);
return;
}
if (left) // 左括号有剩余
backTrack(left - 1, right, str + '(');
if (left < right) // 右括号剩余数量比左括号多
backTrack(left, right - 1, str + ')');
}
backTrack(n, n, '');
return res;
};
复杂度超出分析范畴。
200.岛屿数量:网格dfs (仅字节,前端难度较高)
「DFS 的三要素基本结构」:
-
访问相邻节点:上下左右四个。
-
判断 base case:指的是超出网格范围的格子。「先污染,后治理」,不论当前在哪个格子,先往四个方向走一步再说,如果发现超出了网格范围再赶紧返回。
得出网格 dfs 的遍历框架代码:
const inArea = function(grid, x, y) { // 判断坐标 (x, y) 是否在网格中
return x >= 0 && x < grid.length && y >= 0 && y < grid[0].length;
}
const dfs = function (grid, x, y) { // dfs 遍历网格
if (!inArea(grid, x, y)) {
return;
}
dfs(grid, x - 1, y);
dfs(grid, x + 1, y);
dfs(grid, x, y - 1);
dfs(grid, x, y + 1);
}
- 避免重复遍历(二叉树dfs不需要该要素):网格结构的 dfs 和二叉树的 dfs 最大的不同之处在于,遍历中可能遇到遍历过的节点。因此需要 「标记已经遍历过的格子」,例如对于岛屿问题,我们在值为 1 的陆地格子上做 dfs 遍历,每走过一个陆地格子,就把格子的值改为 2,当遇到 2 的时候,就知道这是遍历过的格子了。
此时的遍历框架加入了 避免重复遍历 的语句,如下:
const dfs = function (grid, x, y) {
if (!inArea(grid, x, y)) {
return;
}
if (grid[x][y] != 1) { // 不是岛屿 或 之前已经遍历过该岛屿
return;
}
grid[x][y] = 2; // 标记
dfs(grid, x - 1, y);
dfs(grid, x + 1, y);
dfs(grid, x, y - 1);
dfs(grid, x, y + 1);
}
如上,我们就得到了一个岛屿问题、乃至各种网格问题的通用 dfs 遍历方法。
对于求岛屿数量的问题,我们先扫描整个二维网格,如果一个位置为 1 ,则以其为起始节点开始进行 dfs 搜索,在搜索过程中,每个搜索到的 1 都会被重新标记为 0。「最终岛屿的数量就是我们进行深度优先搜索的次数」,另外,注意字符串!
// dfs 深搜,将遍历过的地方赋值为'0'
const dfs = function(grid, x, y) {
if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == '0') {
return; // 越界 或 遇到海洋 就停止搜索
}
grid[x][y] = '0'; // 标记 已经遍历过的地方
dfs(grid, x + 1, y);
dfs(grid, x - 1, y);
dfs(grid, x, y + 1);
dfs(grid, x, y - 1);
}
const numIslands = function(grid) {
let res = 0; // 岛屿数量
for (let i = 0; i < grid.length; i++) { // 遍历二维网格
for (let j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
时间复杂度:O(mn),其中m是行数,n是列数,我们访问每个网格最多一次。
空间复杂度:O(mn),递归的深度最大可能是整个网格的大小。
695. 岛屿的最大面积:网格dfs - 仅字节
思路与 200.岛屿问题 相同。
const dfs = function(grid, x, y) {
if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == 0) {
return 0; // 注意! 返回0
}
grid[x][y] = 0;
return 1 // 注意,return +
+ dfs(grid, x + 1, y)
+ dfs(grid, x - 1, y)
+ dfs(grid, x, y + 1)
+ dfs(grid, x, y - 1);
}
const maxAreaOfIsland = function(grid) {
let res = 0; // 岛屿的最大面积
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
let tmp = dfs(grid, i, j); // 注意
res = Math.max(tmp, res); // 注意
}
}
}
return res;
}
时间复杂度:O(mn)。
空间复杂度:O(mn)。
39. 组合总和:回溯,考察很少
因为本题没有组合数量要求,所以递归没有层数的限制,只要选取的元素总和超过 target就返回!和46题思路类似。
const combinationSum = function(nums, target) {
const res = [];
const path = []; // 存放符合条件的结果
const backtracking = function(start, sum) { // sum 统计 path 里的元素和
if (sum > target) return; // 递归终止1
if (sum == target) { // 递归终止2
res.push(path.slice());
return;
}
// 子集问题是无序的,所以for循环从start开始
for (let i = start; i < nums.length; i++) {
let val = nums[i];
if (val > target - sum) continue;
path.push(val);
sum += val;
backtracking(i, sum);
path.pop();
sum -= val;
}
}
backtracking(0, 0);
return res;
};
时间复杂度:取决于搜索树所有叶子节点的深度之和。
空间复杂度:O(target)。取决于递归的栈深度,最差情况需要递归 target 层。