46. 全排列
LeetCode 461 · 中等 · 回溯
题
给一个元素互不相同的整数数组,返回它所有可能的排列,顺序任意。
例:nums = [1, 2, 3] → [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]。
一句话
一层填一个位置,每层把「还没用过的数」挨个放上去、递归、再拿下来——决策树的每条根到叶路径就是一个排列。
关键技巧
回溯 = 在决策树上做 DFS,并在回退时撤销选择。 状态只有三样:已选的路径 path、还能选什么、何时收集答案。排列的「还能选什么」是「不在 path 里的数」,用一个 used 布尔数组 \(O(1)\) 判定。
graph TD
R["[]"] --> A["[1]"]
R --> B["[2]"]
R --> C["[3]"]
A --> A2["[1,2]"] --> A3["[1,2,3]"]
A --> A4["[1,3]"] --> A5["[1,3,2]"]
B --> B2["[2,1]"] --> B3["[2,1,3]"]
B --> B4["[2,3]"] --> B5["[2,3,1]"]
C --> C2["[3,1]"] --> C3["[3,1,2]"]
C --> C4["[3,2]"] --> C5["[3,2,1]"] 为什么对。 第 \(k\) 层恰好有 \(n-k\) 个可选分支,叶子数 \(n!\),且每条路径上元素两两不同——不重不漏。
撤销必须对称。 递归前 append + used[i]=True,递归后 pop + used[i]=False;收集答案时要拷贝 path[:],否则收进去的是同一个会被继续修改的列表。
交换法省掉 used:把 nums[first:] 当作候选区,把第 i 个换到 first 位置即表示「这一位选它」,递归完再换回来。
解
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
res, path, used = [], [], [False] * n
def dfs():
if len(path) == n:
res.append(path[:])
return
for i in range(n):
if used[i]:
continue
used[i] = True
path.append(nums[i])
dfs()
path.pop()
used[i] = False
dfs()
return res
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
nums, res = nums[:], []
def dfs(first):
if first == len(nums):
res.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first]
dfs(first + 1)
nums[first], nums[i] = nums[i], nums[first]
dfs(0)
return res
时间 \(O(n \cdot n!)\)(\(n!\) 个叶子,每个拷贝 \(O(n)\)),空间 \(O(n)\)(递归栈与路径,不计输出)。
延伸
- 有重复元素(LeetCode 47):先排序,同一层里
nums[i] == nums[i-1] and not used[i-1]时跳过——保证相同值只按固定次序出场。 - 排列 vs 组合 vs 子集:排列每层从头扫、靠
used去重;组合/子集每层从start往后扫、靠顺序去重——对比 78. 子集、39. 组合总和。 - 只要「下一个」排列而不是全部,用 31. 下一个排列 的 \(O(n)\) 原地法。
自测
-
LeetCode 46,2026-09-29 读。 ↩