题目描述:
给定一个不含重复数字的数组
nums,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]示例 3:
输入:nums = [1] 输出:[[1]]
解题思路:
方法一:回溯 + used 数组
核心思路:
把问题看成树形结构:
-
每一层选择一个数字
-
每个数字只能用一次
-
用
used数组标记哪些数字已经被选
具体过程示例:
nums = [1,2,3]
[]
/ | \
1 2 3
/ \ / \ / \
2 3 1 3 1 2
| | | | | |
3 2 3 1 2 1
所有路径:
1→2→3, 1→3→2
2→1→3, 2→3→1
3→1→2, 3→2→1
代码实现:
cpp
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
vector<int> path;
vector<bool> used(nums.size(), false);
backtrack(nums, used, path, result);
return result;
}
private:
void backtrack(vector<int>& nums, vector<bool>& used,
vector<int>& path, vector<vector<int>>& result) {
// 终止条件:path 长度等于 nums 长度
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
// 遍历所有数字
for (int i = 0; i < nums.size(); i++) {
if (used[i]) continue; // 已使用,跳过
used[i] = true; // 标记已使用
path.push_back(nums[i]); // 选择
backtrack(nums, used, path, result); // 递归
path.pop_back(); // 撤销
used[i] = false; // 取消标记
}
}
};
复杂度分析:
设 n 是数组长度。
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n × n!) | n! 个排列,每个排列长度 n |
| 空间复杂度 | O(n) | 递归栈深度 + path + used |
关键细节:
- 为什么用
used数组?
因为每个元素只能用一次,需要标记哪些元素已经被选。
-
used[i] = true:表示nums[i]已经被选 -
used[i] = false:表示nums[i]还没被选
- 为什么
path.size() == nums.size()是终止条件?
当 path 长度等于 nums 长度时,说明所有元素都被选了,得到一个完整排列。
- 和「组合总和」的区别
| 题目 | 区别 |
|---|---|
| 39. 组合总和 | 可重复选,用 start 避免重复组合 |
| 46. 全排列 | 不可重复选,用 used 标记已选 |
| 78. 子集 | 不可重复选,用 start 控制顺序 |
关键区别:
-
组合:顺序不重要,用
start避免重复 -
排列:顺序重要,用
used标记已选
方法二:交换法(不需要 used 数组)
思路:把数组分成"已选"和"未选"两部分,通过交换把未选的元素换到当前位置。
代码实现:
cpp
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
backtrack(nums, 0, result);
return result;
}
private:
void backtrack(vector<int>& nums, int start, vector<vector<int>>& result) {
if (start == nums.size()) {
result.push_back(nums);
return;
}
for (int i = start; i < nums.size(); i++) {
swap(nums[start], nums[i]); // 选择
backtrack(nums, start + 1, result); // 递归
swap(nums[start], nums[i]); // 撤销
}
}
};
优点 :不需要 used 数组,空间更省。
缺点:修改了原数组(但会恢复)。
方法三:插入法(迭代)
思路:逐个插入元素,生成所有排列。
代码实现:
cpp
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result = {{}};
for (int num : nums) {
vector<vector<int>> next;
for (auto& perm : result) {
for (int i = 0; i <= perm.size(); i++) {
vector<int> newPerm = perm;
newPerm.insert(newPerm.begin() + i, num);
next.push_back(newPerm);
}
}
result = move(next);
}
return result;
}
};
复杂度:时间 O(n × n!),空间 O(n × n!)
三种方法对比:
| 方法 | 时间复杂度 | 空间复杂度 | 是否修改原数组 | 推荐度 |
|---|---|---|---|---|
| 回溯 + used | O(n × n!) | O(n) | ❌ 不修改 | ⭐⭐⭐⭐⭐ |
| 交换法 | O(n × n!) | O(n) | ✅ 修改(会恢复) | ⭐⭐⭐⭐ |
| 插入法 | O(n × n!) | O(n × n!) | ❌ 不修改 | ⭐⭐⭐ |
总结:
| 要点 | 说明 |
|---|---|
| 核心思想 | 回溯:每层选一个未使用的数字 |
| 关键操作 | used[i] = true → 递归 → used[i] = false |
| 终止条件 | path.size() == nums.size() |
| 时间复杂度 | O(n × n!) |
| 空间复杂度 | O(n) |