46. 全排列
题目
给定一个不含重复数字的数组 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]]
提示:
- 1 <= nums.length <= 6
- -10 <= nums[i] <= 10
- nums 中的所有整数 互不相同
解法一:回溯法(使用标记数组)
解题思路
这种方法通过递归构建排列,使用标记数组记录元素是否已被使用,核心步骤包括:
- 从空序列开始,尝试添加未使用的元素
- 当序列长度等于数组长度时,记录当前排列
- 回溯:移除最后添加的元素,标记为未使用,尝试其他元素
java
import java.util.*;
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> current = new ArrayList<>();
boolean[] used = new boolean[nums.length]; // 标记元素是否已使用
backtrack(nums, used, current, result);
return result;
}
private void backtrack(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
// 终止条件:当前排列长度等于数组长度
if (current.size() == nums.length) {
result.add(new ArrayList<>(current)); // 添加副本到结果集
return;
}
// 遍历所有元素,尝试使用未被使用的元素
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
// 选择当前元素
used[i] = true;
current.add(nums[i]);
// 递归处理剩余元素
backtrack(nums, used, current, result);
// 回溯:撤销选择
current.remove(current.size() - 1);
used[i] = false;
}
}
}
}
算法解析
以 nums = [1,2,3]
为例:
- 初始状态:
current = []
,used = [false, false, false]
- 第一层递归:尝试添加1、2、3中的一个,标记为已使用
- 第二层递归:从剩余元素中选择一个添加
- 第三层递归:添加最后一个剩余元素,此时序列长度为3,添加到结果
- 回溯过程:逐层移除最后添加的元素,标记为未使用,尝试其他可能性
该方法的时间复杂度为 O(n×n!),需要遍历n!个排列,每个排列需要O(n)时间复制;空间复杂度为 O(n),主要是递归栈和标记数组的开销。
解法二:邻位交换法(无标记数组)
解题思路
这种方法通过交换元素位置来生成排列,无需标记数组,核心步骤包括:
- 固定当前位置的元素(通过与后续元素交换)
- 递归处理剩余位置的元素
- 回溯:交换回元素,恢复数组原状,尝试其他交换组合
java
import java.util.*;
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}
backtrackSwap(list, 0, result);
return result;
}
private void backtrackSwap(List<Integer> list, int index, List<List<Integer>> result) {
// 终止条件:当前索引达到列表末尾
if (index == list.size()) {
result.add(new ArrayList<>(list));
return;
}
// 从当前索引开始,与后续每个元素交换
for (int i = index; i < list.size(); i++) {
// 交换当前索引与i位置的元素
Collections.swap(list, index, i);
// 递归处理下一个位置
backtrackSwap(list, index + 1, result);
// 回溯:交换回来恢复原状
Collections.swap(list, index, i);
}
}
}
算法解析
以 nums = [1,2,3]
为例:
- 初始状态:
list = [1,2,3]
,index = 0
- 第一层递归:将index=0分别与i=0、1、2交换,得到[1,2,3]、[2,1,3]、[3,2,1]
- 第二层递归:对每种情况,处理index=1的位置,继续交换
- 第三层递归:处理index=2的位置,完成排列并添加到结果
- 回溯过程:逐层交换回元素,恢复数组状态,尝试其他交换组合
该方法的时间复杂度同样为 O(n×n!),空间复杂度为 O(n),但省去了标记数组的空间,实际空间效率更高。
两种方法对比
特性 | 标准回溯法(标记数组) | 邻位交换法(无标记) |
---|---|---|
核心操作 | 标记数组+添加/删除元素 | 元素交换 |
空间效率 | 稍低(需要标记数组) | 更高(无标记数组) |
直观性 | 更容易理解 | 稍难理解(依赖交换逻辑) |
适用场景 | 所有可迭代序列 | 主要适用于数组/列表 |
两种方法本质上都是深度优先搜索(DFS),通过递归探索所有可能的排列组合,是解决全排列问题的经典方法。