你这道「全排列」的问题,不是不会思路,而是回溯代码的顺序和细节容易写乱。
你主要有这几个问题:
1. 结束条件位置放错了
你把:
if (result.size() == n) {
list.add(result);
return;
}
写在了 for 循环里面。
但正确习惯是:递归函数一进来,先判断是否已经完成。
应该写在 for 前面:
if (result.size() == nums.length) {
list.add(new ArrayList<>(result));
return;
}
原因是:
每次进入 dfs,都先判断当前路径是不是已经凑够长度了。
凑够了就保存答案,不需要再进入 for 循环。
2. 没有复制 result
你写的是:
list.add(result);
这是错的。
因为 result 是一个会不断变化的列表,后面还会添加、删除。
正确写法:
list.add(new ArrayList<>(result));
意思是:
把当前 result 复制一份保存起来。
否则最后答案可能全都变成空数组,或者内容混乱。
3. 递归调用的位置不严谨
你原来写的是:
if (!used[i]) {
result.add(nums[i]);
used[i] = true;
}
dfs(nums);
result.remove(nums[i]);
used[i] = false;
问题是:
即使 used[i] == true,没有选择这个数,你还是会执行:
dfs(nums);
这会导致逻辑混乱。
正确写法是:
if (used[i]) {
continue;
}
result.add(nums[i]);
used[i] = true;
dfs(nums);
result.remove(result.size() - 1);
used[i] = false;
也就是说:
用过的数字直接跳过。
没用过的数字,才执行:选择 → 递归 → 撤销。
4. 删除元素写错了
你写的是:
result.remove(nums[i]);
这个很容易出错。
因为在 Java 的 List<Integer> 里:
remove(数字)
会优先理解成:
删除这个下标位置的元素
而不是删除这个值。
比如:
result.remove(2);
它的意思可能是:
删除下标为 2 的元素
不是删除数字 2。
正确写法:
result.remove(result.size() - 1);
因为回溯时,我们一定是删除刚刚加入的最后一个数字。
5. 回溯模板顺序还不熟
你现在最大的问题是:知道要递归,但不知道代码应该按什么顺序摆。
全排列的固定顺序是:
1. 判断是否完成
2. for 循环枚举每个数字
3. 如果用过,跳过
4. 做选择
5. 递归
6. 撤销选择
对应代码:
public void dfs(int[] nums) {
if (result.size() == nums.length) {
list.add(new ArrayList<>(result));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
result.add(nums[i]);
used[i] = true;
dfs(nums);
result.remove(result.size() - 1);
used[i] = false;
}
}
一句话总结你的问题
你已经知道要用:
result
used
dfs
说明大方向是对的。
你卡住的是:
回溯不是随便递归,而是必须严格按照
判断结束 → 枚举选择 → 做选择 → 递归 → 撤销选择
这个顺序写。
以后你写全排列,先默写这个模板:
if (路径长度够了) {
保存路径的拷贝;
return;
}
for (每一个选择) {
if (这个选择不能用) {
continue;
}
做选择;
dfs();
撤销选择;
}
你这次的核心错误可以记成四句话:
结束条件放 for 前面
保存答案要 new ArrayList
用过的数字直接 continue
撤销时删除最后一个元素