一、题目要求
给定一个 不含重复元素 的整数数组 nums,返回它的 所有可能的全排列。
示例:
输入:nums = [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
题目关注点并不在排序或数学公式,而在于:
- 如何枚举所有可能情况
- 如何在递归过程中避免重复使用同一个元素
- 如何正确保存每一种排列结果
二、整体解题思路
这道题是**回溯(Backtracking)**的标准模型题,可以用一句话概括:
在一棵「决策树」上做深度优先遍历,走到叶子节点时收集答案,回退时撤销选择。
拆成三个核心问题:
- 如何表示“当前正在构造的排列”?
使用一个list,表示当前路径。 - 如何避免一个数字在同一排列中被重复使用?
使用visited记录某个数字是否已经用过。 - 什么时候可以把当前结果加入答案?
当list.size() == nums.length,说明一个完整排列已经构造完成。
三、完整代码
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
HashMap<Integer, Boolean> visited = new HashMap<>();
// 初始化访问状态
for (int num : nums) {
visited.put(num, false);
}
backtrack(res, nums, visited, new ArrayList<>());
return res;
}
private void backtrack(List<List<Integer>> res,
int[] nums,
HashMap<Integer, Boolean> visited,
List<Integer> list) {
// 递归终止条件:形成一个完整排列
if (list.size() == nums.length) {
res.add(new ArrayList<>(list));
return;
}
// 尝试选择每一个还没用过的数字
for (int num : nums) {
if (!visited.get(num)) {
list.add(num);
visited.put(num, true);
backtrack(res, nums, visited, list);
// 回溯:撤销选择
list.remove(list.size() - 1);
visited.put(num, false);
}
}
}
}
四、回溯过程逐层拆解
我们以 nums = [1,2,3] 为例,看代码到底在“跑什么”。
1. 初始状态
list = []
visited = {1:false, 2:false, 3:false}
此时还没选任何数字。
2. 第一层递归:选择第 1 个位置
for 循环遍历 nums:
-
选 1
list = [1] visited[1] = true
进入下一层递归。
3. 第二层递归:选择第 2 个位置
此时可选的只剩 {2,3}:
-
选 2
list = [1,2] visited[2] = true
继续递归。
4. 第三层递归:选择第 3 个位置
只剩下 3:
list = [1,2,3]
visited[3] = true
现在:
list.size() == nums.length
说明这是一个完整排列。
5. 为什么这里一定要 new?
res.add(new ArrayList<>(list));
此时 list 是一个会被继续修改的工作区。
如果直接 res.add(list):
- 后续
remove会把已经存入res的内容一起改掉 - 最终结果会全部变成空列表或重复内容
new ArrayList<>(list) 的含义是:
把当前路径的“快照”复制一份,永久保存下来。
6. 回溯(撤销选择)
list.remove(list.size() - 1);
visited.put(num, false);
这一步非常关键,它做了两件事:
- 把当前数字从排列中移除
- 标记该数字“可以被重新使用”
然后返回上一层,尝试新的分支。
五、visited 的作用本质
visited 并不是为了“去重排列”,而是为了:
保证一个数字在同一个排列中只出现一次
它限制的是「路径内部」,不是结果集合。
每次回溯结束后,visited 都会被恢复到进入该递归前的状态。
六、复杂度分析
-
时间复杂度:
O(n × n!)- 一共有
n!种排列 - 每次构造/复制需要
O(n)
- 一共有
-
空间复杂度:
O(n)- 递归深度为
n list和visited只占用线性空间
- 递归深度为
七、总结
这道全排列题目,本质上是在训练三件事:
- 如何用递归表示“选择 → 递进 → 撤销”
- 如何区分“工作区状态”和“最终结果”
- 如何理解回溯不是暴力,而是受约束的 DFS