46. 全排列

LeetCode 原题链接

题目描述

给定一个不含重复数字的数组 nums,返回其所有可能的全排列。

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

题型判断

每个位置都要从“尚未使用的数字”中选择一个,选择后继续处理下一个位置;走到底后撤销选择并尝试其他分支。这是典型回溯。

全排列与组合、子集问题的一个重要区别是:每一层都可以从整个数组中选择元素,只要该元素没有出现在当前路径中。因此循环每次都从索引 0 开始,不能使用 startIndex 限制选择范围。

例如生成 [2,1,3] 时,选择 2 之后,下一层仍然需要回头选择索引更小的 1。如果只允许向后选择,就会漏掉这个排列。

核心思路

递归过程中维护:

  • path:当前已经选择的排列前缀。
  • used[i]nums[i] 是否已经出现在当前路径中。
  • path.length === nums.length 时,保存路径副本。

在每次进入递归时,都应满足以下状态关系:

used[i] 为 true ⇔ nums[i] 已经存在于当前 path 中

所以一次完整选择必须包含“做选择、继续递归、撤销选择”三个步骤:

used[i] = true;
path.push(nums[i]);

backtrack();

path.pop();
used[i] = false;

nums = [1,2,3] 为例,递归树如下:

[]
├─ [1]
│  ├─ [1,2]
│  │  └─ [1,2,3]
│  └─ [1,3]
│     └─ [1,3,2]
├─ [2]
│  ├─ [2,1]
│  │  └─ [2,1,3]
│  └─ [2,3]
│     └─ [2,3,1]
└─ [3]
   ├─ [3,1]
   │  └─ [3,1,2]
   └─ [3,2]
      └─ [3,2,1]

每深入一层,就确定排列中的一个位置;当路径长度等于 nums.length 时,得到一个完整排列。

代码实现

var permute = function (nums) {
  const result = [];
  const path = [];
  const used = new Array(nums.length).fill(false);

  const backtrack = () => {
    if (path.length === nums.length) {
      result.push([...path]);
      return;
    }

    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue;

      used[i] = true;
      path.push(nums[i]);

      backtrack();

      path.pop();
      used[i] = false;
    }
  };

  backtrack();
  return result;
};

复杂度与易错点

  • 时间复杂度:O(n × n!),共有 n! 个排列,复制每个排列需要 O(n)
  • 辅助空间复杂度:O(n),路径、标记数组和递归深度均为线性。
  • 返回结果占用 O(n × n!) 空间,共有 n! 个长度为 n 的排列。
  • 保存结果时必须复制 path,否则所有结果会引用同一个数组。
  • 回溯返回后必须同时撤销 pathused 状态。
  • 每层循环必须从 0 开始;startIndex 适用于组合、子集等只向后选择的问题,不适用于全排列。

如果数组包含重复元素

本题保证数组元素互不相同,所以只需要通过 used[i] 防止同一个元素在一条路径中被重复选择。

如果数组允许包含重复元素,仅使用 used 仍然会生成重复排列。此时需要先排序,再增加同层去重:

nums.sort((a, b) => a - b);

for (let i = 0; i < nums.length; i++) {
  if (used[i]) continue;

  if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) {
    continue;
  }

  // 做选择、递归、撤销选择
}

两个判断的职责不同:

  • used[i]:防止同一个下标的元素在当前路径中被重复选择。
  • nums[i] === nums[i - 1] && !used[i - 1]:跳过同一层中值相同的重复选择,避免生成重复排列。