文章目录
- 一、题目理解
- 二、思路总览
- [三、最常用方法:回溯 + used 数组](#三、最常用方法:回溯 + used 数组)
-
- [1. 核心思想](#1. 核心思想)
- [2. 算法流程图](#2. 算法流程图)
- [3. 复杂度](#3. 复杂度)
- [4. Java 代码](#4. Java 代码)
- 四、原地交换回溯(更节省辅助空间)
-
- [1. 核心思想](#1. 核心思想)
- [2. 特点](#2. 特点)
- [3. 复杂度](#3. 复杂度)
- [4. Java 代码](#4. Java 代码)
- 五、迭代构造(逐步插入法)
-
- [1. 核心思想](#1. 核心思想)
- [2. 复杂度](#2. 复杂度)
- [3. Java 代码](#3. Java 代码)
- 六、字典序生成:next_permutation
-
- [1. 核心思想](#1. 核心思想)
- [2. 复杂度](#2. 复杂度)
- [3. Java 代码](#3. Java 代码)
- 七、过程可视化示例
一、题目理解
- 描述:给定一个不含重复数字的数组 nums,返回其所有可能的全排列。
- 要点:
- 元素互不相同
- 需要列出所有排列,顺序不限
- 输出是所有排列的集合,每个排列长度等于数组长度
示例
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
二、思路总览
全排列解法总览
回溯 + used数组
原理: 逐位选择未使用数字
状态: 路径path, 标记used
终止: path长度==n
时间: O(n·n!)
空间: O(n)(不含结果)
回溯 + 原地交换
原理: 固定位置first, 与后面位置交换
优点: 无需额外used数组
时间: O(n·n!)
空间: O(n)(递归栈, 原地操作)
迭代插入法
原理: 逐个数字插入已有排列的每个位置
时间: O(n·n!)
空间: O(n·n!)(构建过程中)
字典序 next_permutation
原理: 先排序, 反复调用下一个排列
前提: 所有数字不同
时间: O(n·n!)
空间: O(1)(原地, 不含结果)
三、最常用方法:回溯 + used 数组
1. 核心思想
- 把排列构建看作一条"路径"。每一层递归在当前路径后面选择一个还没用过的数字,直到路径长度等于 n。
- 三要素:
- 选择:挑一个未使用的数字 nums[i]
- 约束:used[i] 为 false 才能选
- 结束:path.size() == n 时收集答案
2. 算法流程图
是
否
否
是
开始
初始化 path, used, res
path.size()==n?
收集 path 的拷贝到 res
返回上一层
遍历 i=0..n-1
used[i]==false?
选择: used[i]=true, path.add(nums[i])
撤销: path.removeLast(), used[i]=false
3. 复杂度
- 时间:O(n · n!)。共有 n! 个排列,每次生成/拷贝一条路径成本 O(n)。
- 空间:O(n) 递归栈 + O(n) used 数组(不含结果存储)。
4. Java 代码
java
import java.util.*;
public class SolutionUsed {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
boolean[] used = new boolean[nums.length];
Deque<Integer> path = new ArrayDeque<>();
backtrack(nums, used, path, res);
return res;
// 结果包含 n! 条列表,每条长度 n
}
private void backtrack(int[] nums, boolean[] used, Deque<Integer> path, List<List<Integer>> res) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
// 选择
used[i] = true;
path.addLast(nums[i]);
// 递归
backtrack(nums, used, path, res);
// 撤销选择
path.removeLast();
used[i] = false;
}
}
}
四、原地交换回溯(更节省辅助空间)
1. 核心思想
- 用 first 表示当前需要固定的位置,从 first 到末尾依次把每个数字换到 first,再递归固定下一个位置。
- 递归返回后再交换回来(恢复现场)。
2. 特点
- 无需 used[],用交换保证位置不重复使用。
- 对原数组进行原地变换,空间更省。
3. 复杂度
- 时间:O(n · n!)
- 空间:O(n) 递归栈(原地操作没有额外结构)
4. Java 代码
java
import java.util.*;
public class SolutionSwap {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, 0, res);
return res;
}
private void backtrack(int[] nums, int first, List<List<Integer>> res) {
if (first == nums.length) {
List<Integer> perm = new ArrayList<>(nums.length);
for (int v : nums) perm.add(v);
res.add(perm);
return;
}
for (int i = first; i < nums.length; i++) {
swap(nums, first, i);
backtrack(nums, first + 1, res);
swap(nums, first, i); // 恢复
}
}
private void swap(int[] a, int i, int j) {
if (i == j) return;
int t = a[i]; a[i] = a[j]; a[j] = t;
}
}
五、迭代构造(逐步插入法)
1. 核心思想
- 从空排列开始,依次把 nums 的每个数字,插入到当前所有排列的每一个位置,构成新的全体排列。
- 例如:已有 [ [1,2] ],插入 3 得到 [ [3,1,2], [1,3,2], [1,2,3] ]。
2. 复杂度
- 时间:O(n · n!)。每加入一个数,需要对当前所有排列进行 O(长度) 次插入。
- 空间:O(n · n!)。需要存放所有中间与最终结果。
3. Java 代码
java
import java.util.*;
public class SolutionIterativeInsert {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
res.add(new ArrayList<>()); // 从空排列开始
for (int num : nums) {
List<List<Integer>> next = new ArrayList<>();
for (List<Integer> perm : res) {
for (int pos = 0; pos <= perm.size(); pos++) {
List<Integer> newPerm = new ArrayList<>(perm);
newPerm.add(pos, num);
next.add(newPerm);
}
}
res = next;
}
return res;
}
}
六、字典序生成:next_permutation
1. 核心思想
- 先按升序排序 nums,即得到字典序最小的排列。
- 反复调用"下一个排列"的原地算法,直到没有下一个为止。
- 下一个排列步骤:
- 从右往左找到第一个 nums[i] < nums[i+1] 的位置 i
- 从右往左找到第一个 nums[j] > nums[i] 的位置 j
- 交换 nums[i], nums[j]
- 反转区间 [i+1, n-1]
2. 复杂度
- 单次 next_permutation:O(n)
- 总时间:O(n · n!)(要生成 n! 次)
- 额外空间:O(1)(原地,不含结果)
3. Java 代码
java
import java.util.*;
public class SolutionNextPermutation {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
do {
List<Integer> cur = new ArrayList<>(nums.length);
for (int v : nums) cur.add(v);
res.add(cur);
} while (nextPermutation(nums));
return res;
}
// 返回是否存在下一个排列
private boolean nextPermutation(int[] a) {
int n = a.length;
// 1) 寻找下降点
int i = n - 2;
while (i >= 0 && a[i] >= a[i + 1]) i--;
if (i < 0) return false; // 已是最大排列
// 2) 从右找第一个 > a[i] 的
int j = n - 1;
while (a[j] <= a[i]) j--;
// 3) 交换
swap(a, i, j);
// 4) 反转后缀
reverse(a, i + 1, n - 1);
return true;
}
private void swap(int[] a, int i, int j) {
int t = a[i]; a[i] = a[j]; a[j] = t;
}
private void reverse(int[] a, int l, int r) {
while (l < r) swap(a, l++, r--);
}
}
七、过程可视化示例
以回溯 + used 数组方法,nums = [1,2,3],状态树(部分展开):
开始: path=[]
path=[1]
path=[2]
path=[3]
path=[1,2]
path=[1,3]
path=[1,2,3] 收集
path=[1,3,2] 收集
path=[2,1]
path=[2,3]
path=[3,1]
path=[3,2]
next_permutation 生成 [1,2,3] → [1,3,2] → [2,1,3] → [2,3,1] → [3,1,2] → [3,2,1] 的时序过程:
数组 next_permutation 数组 next_permutation 初始 [1,2,3] 得到 [1,3,2] 依次得到 [2,1,3], [2,3,1], [3,1,2], [3,2,1] 找 i: 从右往左第一个 a[i] < a[i+1] 找 j: 从右往左第一个 a[j] > a[i] 交换 a[i], a[j] 反转 [i+1, 末尾] 重复上述步骤 返回 false 时结束
- 全排列的本质是对长度为 n 的位置进行全排列搜索,典型解法是回溯。
- 四种常见实现:
- 回溯 + used 数组(最通用)
- 原地交换回溯(空间节省)
- 迭代插入法(非递归)
- 字典序 next_permutation(顺序生成)
- 复杂度下界由输出规模决定:时间 O(n · n!),空间至少要容纳 n! 条结果。选择实现时更多考虑代码风格、可读性和使用场景。