本文概览本文以LeetCode题目全排列为例讲解回溯法的核心思路——已选择/未选择的划分visited数组维护顺序回溯就是撤销选择换下一个一、题目二、题目分析题目要求给定一个没有重复数字的数组返回所有可能的全排列全排列其实是初高中常遇到的数学题——n 个元素能排成多少个序列答案是 n!。过程是这样的第1次选择从 n 个元素里选 1 个 → n 种 第2次选择从剩下的 n-1 个里选 1 个 → n-1 种 第3次选择从剩下的 n-2 个里选 1 个 → n-2 种 ... 第n次选择只剩 1 个没得选 → 1 种 总排列数 n × (n-1 × (n-2 × ... × 1)) n!这题的难点不是思路而是怎么不重不漏地遍历完所有排列。如果随便选必然会有重复。所以必须按某种顺序系统地遍历这就需要回溯法思路概览classSolution{publicListListIntegerpermute(int[]nums){// 结果列表ListListIntegerresnewArrayList();if(nums.length0){returnres;}// 访问数组boolean[]visitednewboolean[nums.length];// 递归函数backtrack(res,visited,nums,newArrayList());returnres;}privatevoidbacktrack(ListListIntegerres,boolean[]visited,int[]nums,ArrayListIntegerpath){// 递归出口if(path.size()nums.length){res.add(newArrayList(path));return;}for(inti0;inums.length;i){// 剪枝if(visited[i]){continue;}// 标记访问过visited[i]true;// 添加到当前路径path.add(nums[i]);// 递归调用backtrack(res,visited,nums,path);// 回溯visited[i]false;// 从当前路径中移除path.removeLast();}}}思路简要说明已选择 / 未选择用 path 记录已选的元素用 visited 数组记录每个元素有没有被选过false 没选过。每轮从 i0 扫到末尾跳过选过的选没选过的回溯 撤销选择换下一个递归回来后撤销当前选择visited 设回 falsepath 移除最后一个for 循环 i 自动选下一个元素。这就实现了选完一个换下一个试试三、思路详解第一步已选择 / 未选择的划分回忆前面做过的题——无论 DFS 还是 BFS我们都需要知道已经处理了什么还没处理什么。全排列也一样可以把数组分成两部分已选择已经加入排列的元素用 path 列表记录未选择还没加入排列的元素用 visited 数组标记false 表示未选择以 nums [1, 2, 3] 为例 初始状态 已选择 path [] 未选择 visited [false, false, false] → 1, 2, 3 都可选 选了 1 之后 已选择 path [1] 未选择 visited [true, false, false] → 2, 3 可选 再选了 2 之后 已选择 path [1, 2] 未选择 visited [true, true, false] → 只有 3 可选第二步怎么保证不重不漏这是这题的核心问题。如果随便选比如先选 2 再选 1和先选 1 再选 2可能会产生重复的遍历路径解决办法很简单每一轮都从 i0 开始扫描遇到选过的就跳过选第一个没选过的。因为 for 循环永远从 0 开始选过的元素会被if (visited[i]) continue跳过没选过的元素会按数组下标顺序依次被选中nums [1, 2, 3]假设 1 已经选过了 i0: visited[0]true → 跳过 i1: visited[1]false → 选 2 i2: visited[2]false → 选 3 没选过的 2、3 会按数组顺序被选到不会乱每次都是按固定顺序选不可能产生重复第三步回溯是什么回溯就是撤销当前选择换下一个试试用 nums [1, 2, 3] 举例手动模拟一遍全过程第一轮全选第一个 选 1 → path [1] 选 2 → path [1, 2] 选 3 → path [1, 2, 3] ✓ 第1个排列 回溯撤销 3path [1, 2] 没有其他可选了 回溯撤销 2path [1] 选 3 → path [1, 3] 选 2 → path [1, 3, 2] ✓ 第2个排列 回溯撤销 2path [1, 3] 没有其他可选了 回溯撤销 3path [1] 没有其他可选了 回溯撤销 1path [] 第二轮从倒数第二个开始换 选 2 → path [2] 选 1 → path [2, 1] 选 3 → path [2, 1, 3] ✓ 第3个排列 ... 选 3 → path [2, 3] 选 1 → path [2, 3, 1] ✓ 第4个排列 ... 回溯撤销 2path [] 第三轮换到第一个位置的第三个元素 选 3 → path [3] 选 1 → path [3, 1] 选 2 → path [3, 1, 2] ✓ 第5个排列 ... 选 2 → path [3, 2] 选 1 → path [3, 2, 1] ✓ 第6个排列 ... 回溯撤销 3path []6 个排列正好是 3! 6。观察整个过程第一轮全选第一个得到 [1,2,3]然后从倒数第二个开始回溯换一个选择得到 [1,3,2]再往上一层回溯从倒数第三个开始换选第二个元素 2然后重复第一轮第二轮的操作再从倒数第三个换到第三个元素 3重复操作这就是回溯的本质——从最深处开始撤销换一个选择换完后继续往下走这一层换完了就退到上一层再换第四步代码怎么对应这个过程for(inti0;inums.length;i){if(visited[i])continue;// ① 跳过已选的visited[i]true;// ② 标记选择path.add(nums[i]);// ③ 加入路径backtrack(...);// ④ 往下递归visited[i]false;// ⑤ 回溯撤销标记path.removeLast();// ⑥ 回溯移出路径}①②③选择当前元素④带着这个选择往下走处理剩余元素⑤⑥递归回来后撤销选择for 循环 i 自动选下一个for 循环就是遍历所有可选元素回溯就是选完了换下一个。不需要手动控制从倒数第几个开始换——for 循环 递归自然就实现了这个逻辑第五步递归出口if(path.size()nums.length){res.add(newArrayList(path));return;}当 path 的长度等于 nums 的长度时说明所有元素都选完了这是一个完整的排列。注意要用new ArrayList(path)创建副本——如果直接 add(path)后续回溯修改 path 会影响已经存入的结果第六步完整执行过程图解以 nums [1, 2, 3] 为例用缩进表示递归深度path [] visited [F, F, F] i0: 选 1 → path [1] visited [T, F, F] i0: 跳过已访问 i1: 选 2 → path [1,2] visited [T, T, F] i0: 跳过 i1: 跳过 i2: 选 3 → path [1,2,3] ✓ 加入结果 回溯path [1,2] visited [T, T, F] 回溯path [1] visited [T, F, F] i2: 选 3 → path [1,3] visited [T, F, T] i0: 跳过 i1: 选 2 → path [1,3,2] ✓ 加入结果 回溯path [1,3] 回溯path [1] 回溯path [] visited [F, F, F] i1: 选 2 → path [2] visited [F, T, F] i0: 选 1 → path [2,1] visited [T, T, F] i2: 选 3 → path [2,1,3] ✓ 加入结果 回溯... i2: 选 3 → path [2,3] visited [F, T, T] i0: 选 1 → path [2,3,1] ✓ 加入结果 回溯... 回溯path [] i2: 选 3 → path [3] visited [F, F, T] i0: 选 1 → path [3,1] visited [T, F, T] i1: 选 2 → path [3,1,2] ✓ 加入结果 回溯... i1: 选 2 → path [3,2] visited [F, T, T] i0: 选 1 → path [3,2,1] ✓ 加入结果 回溯... 回溯path []最终结果[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]共 6 个复杂度分析时间复杂度O(n × n!)共 n! 个排列每个排列需要 O(n) 时间复制到结果空间复杂度O(n)递归深度最大为 nvisited 数组和 path 都是 O(n)