021-全排列(回溯法)

46. 全排列-Medium

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

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

方法 1:回溯法(经典解法)

核心思想

  • 递归 + 回溯:每次选择一个未被使用的数字,放入当前路径,直到路径长度等于 nums.length
  • 维护 used 数组:记录哪些数字已经被使用过,避免重复选择。

JavaScript 代码

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 permute = function(nums) {
const result = [];
const used = new Array(nums.length).fill(false); // 记录数字是否被使用

const backtrack = (path) => {
if (path.length === nums.length) {
result.push([...path]); // 复制当前路径
return;
}

for (let i = 0; i < nums.length; i++) {
if (used[i]) continue; // 跳过已使用的数字

used[i] = true; // 标记为已使用
path.push(nums[i]); // 加入当前路径
backtrack(path); // 递归
path.pop(); // 回溯
used[i] = false; // 恢复状态
}
};

backtrack([]);
return result;
};

时间复杂度

  • **O(n × n!)**:共有 n! 种排列,每种排列需要 O(n) 时间生成。

方法 2:交换法(更高效,原地修改)

核心思想

  • 交换元素:通过交换 nums 中的元素,直接生成所有排列,无需 used 数组。
  • 递归 + 回溯:固定一个位置,递归生成剩余部分的排列。

JavaScript 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var permute = function(nums) {
const result = [];

const backtrack = (start) => {
if (start === nums.length) {
result.push([...nums]); // 复制当前排列
return;
}

for (let i = start; i < nums.length; i++) {
[nums[start], nums[i]] = [nums[i], nums[start]]; // 交换
backtrack(start + 1); // 递归
[nums[start], nums[i]] = [nums[i], nums[start]]; // 恢复
}
};

backtrack(0);
return result;
};

时间复杂度

  • **O(n!)**:比方法 1 稍快,但仍是指数级。

方法 3:库函数(Python 适用)

在 Python 中,可以直接用 itertools.permutations

1
2
3
4
from itertools import permutations

def permute(nums):
return list(permutations(nums))

但 JavaScript 没有内置排列函数,需手动实现。


变种问题:全排列 II(含重复数字)

如果 nums 包含重复数字,需去重:

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 permuteUnique = function(nums) {
nums.sort((a, b) => a - b); // 先排序
const result = [];
const used = new Array(nums.length).fill(false);

const backtrack = (path) => {
if (path.length === nums.length) {
result.push([...path]);
return;
}

for (let i = 0; i < nums.length; i++) {
if (used[i] || (i > 0 && nums[i] === nums[i - 1] && !used[i - 1])) {
continue; // 跳过已使用或重复的数字
}

used[i] = true;
path.push(nums[i]);
backtrack(path);
path.pop();
used[i] = false;
}
};

backtrack([]);
return result;
};

关键点

  1. 排序:让相同数字相邻。
  2. 去重条件
    • nums[i] === nums[i - 1]:当前数字和前一个相同。
    • !used[i - 1]:前一个数字未被使用(避免重复选择)。

总结

方法 特点 时间复杂度 适用场景
回溯 + used 数组 直观,易理解 O(n × n!) 通用
交换法 更高效,原地修改 O(n!) 无需额外空间
库函数(Python) 代码最短 - 仅限 Python
全排列 II(去重) 需排序 + 去重逻辑 O(n × n!) 含重复数字

推荐

  • 无重复数字:交换法(方法 2)。
  • 含重复数字:回溯 + 排序去重(方法 1 变种)。

022-单词搜索(回溯法)

单词搜索(Word Search)

给定一个 m x n 的二维字符网格 board 和一个字符串 word,判断 word 是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格(水平或垂直相邻)构成,同一个单元格内的字母不允许被重复使用

示例

1
2
3
4
5
6
7
8
9
10
输入:
board = [
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
],
word = "ABCCED"

输出:true
解释:路径 A → B → C → C → E → D 可以构成单词 "ABCCED"

方法:回溯法(DFS + 剪枝)

核心思想

  1. 遍历网格:对每个单元格作为起点,尝试匹配 word 的第一个字符。
  2. DFS 回溯
    • 从当前单元格出发,向 上、下、左、右 四个方向递归搜索。
    • 如果当前字符匹配,继续搜索下一个字符;否则回溯。
  3. 避免重复使用:用 visited 矩阵或临时修改 board 标记已访问的单元格。

JavaScript 代码

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 exist = function(board, word) {
const m = board.length;
const n = board[0].length;

const dfs = (i, j, index) => {
if (index === word.length) return true; // 全部匹配完成
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== word[index]) {
return false; // 越界或不匹配
}

const temp = board[i][j]; // 临时存储当前字符
board[i][j] = '#'; // 标记为已访问(避免重复使用)

// 向四个方向搜索
const found = 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 found;
};

for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (dfs(i, j, 0)) return true; // 从 (i,j) 开始匹配
}
}
return false;
};

时间复杂度

  • 最坏情况:O(m × n × 4^L),其中 Lword 的长度(每个单元格有 4 个方向的选择)。
  • 剪枝优化:实际运行时,不匹配的路径会提前终止。

关键点

  1. 递归终止条件
    • index === word.length:成功匹配整个单词。
    • 越界或字符不匹配:直接返回 false
  2. 避免重复访问
    • 临时修改 board[i][j] 为特殊字符(如 #),递归结束后恢复。
  3. 方向搜索
    • 每次递归尝试 上、下、左、右 四个方向。

示例解析

以输入 board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]word = "ABCCED" 为例:

  1. (0,0) 开始(A 匹配 word[0]):
    • 向下到 (1,0)SB)→ 回溯。
    • 向右到 (0,1)B 匹配 word[1]):
      • 继续向右到 (0,2)C 匹配 word[2]):
        • 向下到 (1,2)C 匹配 word[3]):
          • 向下到 (2,2)E 匹配 word[4]):
            • 向左到 (2,1)D 匹配 word[5])→ 返回 true

变种问题

单词搜索 II(多个单词)

给定一个 board 和一个单词列表 words,返回所有能在 board 中匹配的单词。
优化方法:使用 Trie 树(字典树) 预处理 words,结合回溯法。

允许重复使用单元格

若允许重复使用单元格,需调整 visited 逻辑(一般不常见)。


总结

方法 时间复杂度 空间复杂度 适用场景
回溯法(DFS) O(m × n × 4^L) O(L)(递归栈) 通用
Trie + 回溯 O(m × n × 4^L) O(T)(Trie 节点数) 多单词搜索

推荐

  • 单单词搜索直接用回溯法。
  • 多单词搜索用 Trie 优化。