在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

全排列

交换式回溯每次固定一个位置:把剩余值交换到 start,递归处理后缀,再重复同一交换,恢复调用者收到的数组。n 个不同值产生 n! 个排列。

每次调用像借用数组,可以重排后缀,但返回时必须原样归还。这样调用者才能尝试下一选择,不必重建状态。允许修改工作数组、需要全部顺序时适合交换法;不便修改共享数组时,可用 path + remaining,但会分配更多临时数组。

递归选择如何工作

nums = [1, 2, 3],先调用 backtrack(0)。位置 0 依次尝试:

  1. 自交换保留 1,递归处理 [2, 3]。
  2. 把 2 换入,递归处理 [1, 3]。
  3. 把 3 换入,递归处理 [2, 1]。

第一个分支的 backtrack(1) 先保留 2,下一层只剩 3,输出 [1, 2, 3]。返回并恢复后,再把 3 换到位置 1,输出 [1, 3, 2]。

根尝试第二个选择前,内部交换已经逐层撤销,根的自交换也已撤销,数组恢复 [1, 2, 3]。动画在此比较保存的调用者快照与返回数组,确认两者相等,再交换位置 0、1。

画布为元素保留稳定身份,重复值也能分清来自哪个位置;代码仍使用普通数值数组。

为什么有 n! 个叶子

第 0 层 n 个选择,第 1 层 n−1 个,最后一层一个:

n * (n - 1) * ... * 1 = n!

三个元素对应 3 * 2 * 1 = 6。每个因子是一层的位置选择,每条完成路径到达一个基础状态。

基础状态必须把当前数组复制到 result。原 nums 还会继续交换;若只追加同一个引用,所有结果会随它改变,最后都变成恢复后的同一顺序。

回溯不变量

对 backtrack(start):每次循环开始前,nums 的顺序都应与本调用刚收到时完全一致。

正向交换构造子状态。子调用可进行很多深层交换,但它返回前先恢复自己的入场状态。当前层再做逆交换,就恢复本层入场状态。

所以一行交换不是独自撤销整棵子树,而是建立在每个后代都履行恢复约定的基础上。去掉逆交换后,只要有三个及以上不同值,就会有排列重复、有排列缺失。[1, 2] 只是碰巧没出错:首个子树只做了自交换,第二个根选择仍从 [1, 2] 开始。只递归而不恢复,不是完整的回溯。

不恢复会怎样

No restore 只去掉第二次交换。[1, 2, 3] 的首个根子树返回 [1, 3, 2],没有恢复 [1, 2, 3],下一兄弟分支从错误状态开始。

仍访问六个叶子,但只有四种值序列:[1, 2, 3]、[1, 3, 2]、[3, 1, 2]、[3, 2, 1]。前两项各重复一次,[2, 1, 3] 和 [2, 3, 1] 完全缺失。基础状态访问六次不代表得到六种排列。

逐步演示

默认 [1, 2, 3] 的完整恢复流程:

  1. 根选 1,自交换;第 1 层保留 2,第 2 层保留 3,复制 [1, 2, 3]。
  2. 撤销两层自交换,第 1 层改选 3,复制 [1, 3, 2];逐层撤销后,根数组恢复 [1, 2, 3]。
  3. 根选 2,交换位置 0、1,得到 [2, 1, 3]。第 1 层依次选 1、3,输出 [2, 1, 3]、[2, 3, 1]。
  4. 子树返回并恢复 [1, 2, 3];根选 3,交换位置 0、2,得到 [3, 2, 1]。
  5. 第 1 层先保留 2,再交换 1、2 两位置,输出 [3, 2, 1]、[3, 1, 2]。
  6. 最后所有逆交换恢复 [1, 2, 3],返回六个独立副本。
根选择内层选择输出
位置 0 放 12,再 3[1, 2, 3]、[1, 3, 2]
位置 0 放 21,再 3[2, 1, 3]、[2, 3, 1]
位置 0 放 32,再 1[3, 2, 1]、[3, 1, 2]

结果栏按首位置的稳定元素身份分组,每组 (n - 1)! 项;调用栈显示当前处理哪个因子。i === start 的自交换是真实步骤,虽然值不动,却让各层使用同一条循环规则。

重复值与唯一排列

算法区分位置。[1, 1, 2] 的两个 1 身份不同,仍有 3! = 6 个位置叶子,但值序列只有 [1, 1, 2]、[1, 2, 1]、[2, 1, 1],每项两次。

重复值预设保留全部六次追加,并把重复结果连接到首次出现,不会暗中去重。若要求唯一排列,应在同一递归深度跳过相同值,常用该层局部 used 集合。先生成全部再对结果去重仍支付了重复的阶乘工作。

时间与空间复杂度

项目界原因
叶子数n!每层有 n−start 个位置选择
时间Θ(n × n!)每叶复制 n 个值
递归辅助空间O(n)最多 n+1 个活动栈帧
返回结果空间Θ(n × n!)n! 个长度 n 的数组

原地交换缩小搜索状态,不能消除输出成本。画布最多四个元素,四元素的 24 个叶子按教学节奏也需约 2 分 35 秒。n = 12 时已有 12! = 479,001,600 个叶子,每个还要复制十二个值。

小规模全排列可用于暴力旅行商路线、字母排列、短排程。实际使用前应确认 n 足够小,或约束能剪去大部分分支,否则完整枚举通常不可行。

边界情况

  • 单元素只有一个排列,经历自交换、基础状态复制和恢复。
  • 空数组通常定义有一个空排列。本课仅接受一到四元素,实际代码可支持数学上的空情况。
  • 负数和重复值都可作为内容,遍历顺序由位置而非数值决定。
  • 反转循环顺序仍可生成不同值输入的同一结果集合,但输出顺序改变。
  • Python 用 nums.copy(),JavaScript 用 [...nums] 保存副本。

常见错误

  • 忘记逆交换。 下一兄弟继承被修改数组;只要有三个及以上不同值,就会有排列重复、有排列缺失。
  • 直接追加可变数组。 多个结果引用同一对象,后续交换改写历史答案。
  • 递归推进 i。 应调用 backtrack(start + 1),固定前缀长度与循环候选索引不同。
  • 用全局 used 做去重。 一分支用过的值在兄弟分支仍可用,去重范围应是当前深度。
  • 把每个叶子都称为唯一值排列。 n! 统计位置选择;只有输入值不同才等于唯一值序列数。

无论采用哪种状态表示,都要选择、递归,并在返回时恢复调用者原有状态。