(LeetCodeHot100)46. 全排列——permutations

46. 全排列——permutations

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

1
2
输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

我的正确答案:DFS + 回溯

  • 一遍过(就是复杂度高了点):
    • 时间:击败15.48%
    • 空间:击败29.46%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<Integer> list = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
getResult(nums, list, ans);
return ans;
}

private void getResult(int[] nums, List<Integer> list, List<List<Integer>> ans) {
if (list.size() == nums.length) {
ans.add(new ArrayList<>(list));
return;
}
// 将没加进去的加进去
for (int i = 0; i < nums.length; i++) {
if (!list.contains(nums[i])) {
list.add(nums[i]);
getResult(nums, list, ans);
// 回溯
list.removeLast();
}

}
}
}

官方解答:DFS + 回溯 + 状态标记

  • 时间复杂度好多了:击败96.58%
  • 空间差不多:击败29.82%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.ArrayList;
import java.util.List;


public class Solution {

public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}

boolean[] used = new boolean[len];
List<Integer> path = new ArrayList<>();

dfs(nums, len, 0, path, used, res);
return res;
}

private void dfs(int[] nums, int len, int depth,
List<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == len) {
res.add(new ArrayList<>(path));
return;
}

// 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
for (int i = 0; i < len; i++) {
if (!used[i]) {
path.add(nums[i]);
used[i] = true;

dfs(nums, len, depth + 1, path, used, res);
// 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
used[i] = false;
path.remove(path.size() - 1);
}
}
}

public static void main(String[] args) {
int[] nums = {1, 2, 3};
Solution solution = new Solution();
List<List<Integer>> lists = solution.permute(nums);
System.out.println(lists);
}
}

一、官方解法核心思路(DFS + 回溯 + 状态标记)

官方解法采用深度优先搜索(DFS)+ 回溯,核心是通过 boolean[] used 数组标记元素是否被使用,避免重复选择,从而生成所有全排列,步骤拆解:

1. 核心变量说明

  • path:保存当前正在构建的排列(比如 [1,2] 是构建 [1,2,3] 的中间状态);
  • used:长度和 nums 一致的布尔数组,used[i] = true 表示 nums[i] 已被加入 path,避免重复选;
  • depth:当前 path 的长度(递归深度),当 depth == nums.length 时,说明已生成一个完整排列,加入结果集;
  • res:最终保存所有全排列的结果集。

2. 递归逻辑(DFS + 回溯)

1
2
3
4
5
6
① 终止条件:depth == nums.length → 保存当前排列,返回;
② 遍历所有元素:
- 若元素未被使用(!used[i]):
✅ 选择:将 nums[i] 加入 path,标记 used[i] = true;
✅ 递归:depth+1,继续构建下一个位置的元素;
✅ 回溯:递归返回后,撤销选择(used[i] = false,path 删除最后一个元素),尝试下一个未被使用的元素。

3. 举例(nums = [1,2,3])

  • 第一层递归(depth=0):选 1 → used [0]=true,path=[1];
  • 第二层递归(depth=1):选 2 → used [1]=true,path=[1,2];
  • 第三层递归(depth=2):选 3 → used [2]=true,path=[1,2,3] → 保存到 res;
  • 回溯:删除 3,used [2]=false → 回到第二层,无其他未用元素 → 再回溯,删除 2,used [1]=false;
  • 第二层重新选 3 → used [2]=true,path=[1,3] → 第三层选 2 → 生成 [1,3,2];
  • 以此类推,遍历所有可能的选择,生成全排列。

二、为什么官方解法时间复杂度更优?

你的解法和官方解法的时间复杂度量级都是 O (n×n!)(n! 是全排列总数,每个排列需 O (n) 时间构建),但实际运行效率官方远优,核心差异在「元素是否被使用的判断效率」:

维度 你的解法(list.contains ()) 官方解法(used [] 数组)
判断元素是否已选 遍历 list 逐个比较,O (n) 时间 直接访问数组下标,O (1) 时间
单轮递归的时间成本 O(n) × O(n) = O(n²) O(n) × O(1) = O(n)
总时间复杂度(实际) O(n² × n!) O(n × n!)

关键细节拆解:

  1. 你的解法的性能瓶颈

    你用 !list.contains(nums[i]) 判断元素是否已加入排列,contains() 是线性查找 —— 每次判断都要遍历 list 中已有的 depth 个元素(最坏 O (n))。

    比如生成 [1,2,3] 时:

    • 选第一个元素:list 为空,contains 是 O (1);

    • 选第二个元素:list 有 1 个元素,contains 是 O (1);

    • 选第三个元素:list 有 2 个元素,contains 是 O (2);

      整体单条排列的判断成本是 O (1+1+2) = O (n),叠加 n! 个排列,总时间是 O (n² × n!)。

  2. 官方解法的优化核心

    boolean[] used 数组标记状态 —— 数组的下标对应 nums 的下标,判断 used[i] 只需直接访问内存地址,是 O (1) 常数时间。

    比如判断 nums[2] 是否已选,只需看 used[2] 的值,无需遍历,单条排列的判断成本是 O (n × 1) = O (n),总时间是 O (n × n!)。

举个直观例子(n=6,题目上限):

  • 你的解法:n² × n! = 36 × 720 = 25920 次操作;

  • 官方解法:n × n! = 6 × 720 = 4320 次操作;

    前者操作数是后者的 6 倍,n 越大,差距越明显(比如 n=5 时,前者是 25×120=3000,后者是 5×120=600)。

三、补充说明(时间复杂度的 “量级” vs “实际”)

从算法复杂度的「量级」来说,两者都属于 O (n×n!)(因为 n² × n! 渐进等价于 n×n!),但渐进等价不代表实际效率相同—— 官方解法的「常数项更小」,这是工程层面的关键优化。