一、前言
全排列是回溯算法的第一道母题 ,也是绝大多数新手最真实的状态:看思路秒懂,一写代码全错。
二、题目描述
给定一个不含重复数字的数组 nums ,返回其所有可能的全排列,可以按任意顺序返回答案。
示例:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
三、错误代码
java
class Solution {
List<List<Integer>> res=new ArrayList<>();
boolean[] used=new boolean[nums.length]; // 致命错误1
List<Integer>path=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
if(nums==null||nums.length==0){
return res;
}
back(nums);
return res;
}
private void back(int[] nums){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return ;
}
for(int i=0;i<nums.length;i++){
if(nums[i]==true){ // 致命错误2
continue;
}
if(nums[i]==false){ // 致命错误3
path.add(nums[i]);
used[i]=true;
back(nums);
path.remove(pathsize()-1); // 致命错误4
used[i]=false;
}
}
}
}
四、逐条复盘我的所有错误
错误1:类成员位置直接 new used 数组
boolean[] used=new boolean[nums.length];
原因:nums 是方法参数,类加载的时候 nums 还不存在,不能用来初始化成员变量。
正确做法:只声明不初始化,在主方法里根据 nums 长度创建数组。
错误2、3:把 nums[i] 当成布尔标记判断
if(nums[i]==true)、if(nums[i]==false)
超级低级逻辑错误:
nums[i] 是数字(1、2、3),不是布尔值!
是否被使用,看的是usedi,不是 numsi。
正确逻辑 :如果 used[i]==true 说明用过了,直接跳过。
错误4:拼写错误 pathsize()
集合获取长度是 path.size(),不能连写,会编译报错。
错误5:多余嵌套 if
只要判断 used[i] 为 true 就 continue,剩下的一定是未使用,不需要再套一层 if 判断 false。
五、标准代码
java
import java.util.ArrayList;
import java.util.List;
class Solution {
// 结果集、当前路径、标记数组(只声明)
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if(nums == null || nums.length == 0){
return res;
}
// 根据输入数组长度初始化标记数组
used = new boolean[nums.length];
back(nums);
return res;
}
// 回溯核心函数
private void back(int[] nums){
// 终止条件:路径长度等于数组长度,说明凑齐一组排列
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
// 遍历所有数字,逐个尝试选择
for(int i = 0; i < nums.length; i++){
// 已经选过的数字,跳过
if(used[i]){
continue;
}
// 1. 做选择:加入路径 + 标记占用
path.add(nums[i]);
used[i] = true;
// 2. 递归:继续选下一个位置的数
back(nums);
// 3. 回溯(撤销选择):递归跑完回来,恢复状态
path.remove(path.size() - 1);
used[i] = false;
}
}
}
六、重点攻克:我最看不懂的四行回溯代码
这四行是回溯的灵魂,也是我最开始完全不懂的地方:
java
path.add(nums[i]);
used[i]=true;
back(nums);
path.remove(path.size()-1);
used[i]=false;
通俗大白话解释(终于听懂版)
第一步:做选择
把当前数字放进路径,标记为已使用,下层递归不能再选。
第二步:递归深挖
程序直接扎进下一层,继续填下一个位置,直到路径填满、记录答案。
第三步:递归返回,必须回溯
当这一条分支的所有排列全部找完了,回到当前行:
删掉刚刚加的数字、取消占用标记,清空状态,换下一个数字重新选。
最简例子走一遍 [1,2]
-
选1 → path=1,used0=true,递归深入
-
下层只能选2 → path=1,2,存答案
-
递归返回,删掉2、取消标记
-
循环走完,回到最上层,删掉1、取消标记
-
状态清空,开始选2开头的排列
不回溯 = 状态永远不重置 = 只能算出第一个答案
七、回溯核心模板(永久背诵)
所有排列题通用模板:
终止条件:收集答案
for 遍历所有选择:
if 已使用 → 跳过
做选择
递归
撤销选择(回溯)
八、总结
-
used 是标记下标,和 nums 数组本身无关,绝对不能拿 nums 判断布尔值。
-
成员变量不能用方法参数初始化,used 必须在主函数 new。
-
回溯核心就是:选进去 → 递归到底 → 退出来重置。
-
存答案必须
new ArrayList<>(path),保存快照,不能直接存 path。 -
撤销顺序必须和选择顺序相反: 选择:add → true 撤销:remove → false