LeetCode 46. 全排列(回溯算法入门模板)
一、题目描述
给定一个 没有重复数字 的数组 nums,返回它的所有可能全排列。
示例
python
输入:nums = [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
二、看到题目先想到什么?
题目要求:
- 把数组里的数字全部用上;
- 每个数字只能使用一次;
- 求所有可能情况。
这种「求所有可能」的问题,通常都可以往**回溯算法(Backtracking)**上想。
回溯算法本质上就是:
尝试一种选择 → 继续往下走 → 走不通或者走到底后撤销选择 → 再尝试下一种选择。
有点像走迷宫:
text
走一步
↓
还能走?
↓
继续走
↓
走到底
↓
退回来
↓
换条路继续走
三、核心思想:深度优先搜索(DFS)
例如:
python
nums = [1,2,3]
第一位可以选:
text
1
2
3
如果第一位选了 1:
text
1
├──2
│ └──3
└──3
└──2
如果第一位选了 2:
text
2
├──1
│ └──3
└──3
└──1
如果第一位选了 3:
text
3
├──1
│ └──2
└──2
└──1
最终得到:
text
123
132
213
231
312
321
四、回溯算法三大核心变量
1、res
保存最终结果。
python
res = []
2、path
当前正在构造的排列。
例如:
python
[1]
[1,2]
[1,2,3]
python
path = []
3、used
记录数字是否已经被使用。
python
used = [False] * len(nums)
例如:
python
nums = [1,2,3]
used = [True,False,True]
表示:
text
1 已使用
2 未使用
3 已使用
五、回溯的固定模板
python
def backtrack():
if 满足结束条件:
保存结果
return
for 选择列表:
做选择
backtrack()
撤销选择
这四步必须记住:
text
做选择
递归
撤销选择
继续下一种选择
六、代入本题
第一步:选择一个数字
例如:
python
path = []
选择:
python
1
变成:
python
path = [1]
并且:
python
used[0] = True
第二步:递归
继续选择下一个数字。
例如:
python
path = [1]
可以选择:
text
2
3
第三步:走到底
得到:
python
path = [1,2,3]
长度已经等于:
python
len(nums)
说明生成了一个完整排列。
保存:
python
res.append(path[:])
第四步:回溯
退回来:
python
path.pop()
used[i] = False
继续尝试其他数字。
七、完整代码
python
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [False] * len(nums)
def backtrack():
# 终止条件
if len(path) == len(nums):
res.append(path[:])
return
# 枚举所有数字
for i in range(len(nums)):
# 已经使用过
if used[i]:
continue
# 做选择
used[i] = True
path.append(nums[i])
# 递归
backtrack()
# 撤销选择
path.pop()
used[i] = False
backtrack()
return res
八、执行过程演示
以:
python
nums = [1,2,3]
为例:
text
[]
│
├──1
│ ├──2
│ │ └──3
│ └──3
│ └──2
│
├──2
│ ├──1
│ │ └──3
│ └──3
│ └──1
│
└──3
├──1
│ └──2
└──2
└──1
最终:
python
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
九、为什么要存副本?
很多同学会这样写:
python
res.append(path)
这是错误的。
因为:
python
path
是一个列表对象。
后面回溯时:
python
path.pop()
会修改原来的列表。
最终结果可能变成:
python
[[], [], [], [], [], []]
或者:
python
[[3,2,1],
[3,2,1],
[3,2,1]]
正确写法:
python
res.append(path[:])
或者:
python
res.append(list(path))
十、高频易错点
1、忘记回溯
错误:
python
path.append(nums[i])
backtrack()
少了:
python
path.pop()
会导致路径越来越长。
2、忘记恢复状态
错误:
python
used[i] = True
backtrack()
少了:
python
used[i] = False
后面的数字无法再次使用。
3、保存结果没用副本
错误:
python
res.append(path)
正确:
python
res.append(path[:])
4、终止条件写错
正确:
python
if len(path) == len(nums):
因为只有路径长度等于数组长度时,才生成一个完整排列。
十一、复杂度分析
时间复杂度
text
O(n × n!)
原因:
- 一共有
n!个排列; - 每个排列保存时需要复制
n个元素。
空间复杂度
text
O(n)
主要来自:
- 递归栈;
- path;
- used数组。
(结果数组不算额外空间。)
十二、回溯题万能模板
python
res = []
path = []
def backtrack():
if 满足结束条件:
res.append(path[:])
return
for 选择列表:
if 当前选择不合法:
continue
# 做选择
path.append(选择)
# 递归
backtrack()
# 撤销选择
path.pop()
十三、一句话总结
回溯算法的核心就是:做选择 → 递归 → 撤销选择。
全排列题目的本质:
枚举每一个位置能放什么数字,走到底得到一种排列,然后回退继续尝试其他可能。
掌握这道题之后,后面的:
- 组合(77)
- 子集(78)
- 组合总和(39)
- N 皇后(51)
都会发现是同一个回溯模板的变形。