
回溯 + 状态标记
回溯的核心在于:尝试选择 -> 递归进入下一层 -> 撤销选择(回退)。
- 路径 (path):记录当前已经选了哪些数字。
- 选择列表:nums 中除去已经在 path 里的数字。
- 结束条件:path 的长度等于 nums 的长度。
- 状态重置:为了让同层级的其他分支能重新使用当前数字,必须在递归返回后将其从 path 中移除。
DFS
eg:nums = [1,2,3]
| 步骤 | 操作 | 当前 path | 当前 used | 动作说明 |
|---|---|---|---|---|
| 1 | dfs() (L1) |
[] |
[F, F, F] |
初始状态 |
| 2 | append(1) |
[1] |
[T, F, F] |
选了 1,去下一层找老二 |
| 3 | append(2) |
[1, 2] |
[T, T, F] |
选了 2,去下一层找老三 |
| 4 | append(3) |
[1, 2, 3] |
[T, T, T] |
选了 3,满了! |
| 5 | SAVE | [1, 2, 3] | [T, T, T] |
存入 res |
| 6 | pop() |
[1, 2] |
[T, T, F] |
回溯:撤销 3 的选择 |
| 7 | pop() |
[1] |
[T, F, F] |
回溯:撤销 2,接下来尝试选 3 做老二 |
| 8 | append(3) |
[1, 3] |
[T, F, T] |
选了 3 做老二 |
| 9 | ... | ... | ... | 重复上述逻辑... |
python
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
res = []
path = [] # 记录当前已经选了哪些数字
used = [False] * n # 布尔数组记录数字是否已被使用
def dfs(): # 不需要传 i
# path达到nums长度,停止
if len(path) == n:
res.append(path[:])
return
# 检查nums里还有哪些能填进path的
for idx, use in enumerate(used):
if not use:
path.append(nums[idx]) # 添加nums[idx]
used[idx] = True # 标记用过
dfs() # 递归,继续下一个坑位
# path达到nums长度,递归结束
path.pop() # 弹出加入的nums[idx](回溯的核心)
used[idx] = False # 重置 nums[idx]为没用过
dfs()
return res
为什么用 path[:]?
在 Python 中,path 是一个列表对象(引用)。如果不使用切片 [:] 拷贝一份副本,最终 res 里的所有元素都会指向同一个被清空的 path 对象。
时间复杂度: O ( n × n ! ) O(n \times n!) O(n×n!)
全排列共有 n ! n! n! 个,每个排列需要 O ( n ) O(n) O(n) 的时间复制到结果集中。
空间复杂度: O ( n ) O(n) O(n)
递归深度为 n n n,且需要 used 数组和 path 数组。
切片
选了 nums[i] 以后,其它所有元素都还能选(除了它自己)
python
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
def backtrack(nums, tmp): #(待选列表,已有排列)
#待选列表为空(所有数字有选完了)
if not nums:
res.append(tmp) # 当前排列加入res
return
# 依次选择待选列表的元素,进入排列。
for i in range(len(nums)):
backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]]) #注意是[nums[i]], 不是nums[i]
backtrack(nums, [])
return res
值传递:这段代码在每次递归时,都通过 + 号产生了全新的列表对象传给下一层。
自动恢复:由于每一层使用的 nums 和 tmp 都是上一层传过来的"快照",当前层对它们的任何操作都不会影响到上一层的变量。当递归函数返回时,上一层的变量依然是原来的样子。
| 维度 | 标准回溯 | 切片传递 |
|---|---|---|
| 空间效率 | 更高 。全局共用一个 path 和 used,空间复杂度 O ( n ) O(n) O(n)。 |
较低。每层都在创建新的列表切片,会产生大量临时对象。 |
| 可读性 | 逻辑略复杂,需要处理"撤销"动作。 | 非常直观,更像纯粹的递归思维。 |
| 性能 | 快。数组原地修改速度最快。 | 慢 。列表拼接和切片操作在 Python 里是 O ( k ) O(k) O(k) 的开销。 |