文章摘要:
- 本文解析了LeetCode全排列问题,通过递归回溯法生成所有不重复排列。算法使用决策树模型,通过标记数组避免重复选择,递归过程中维护路径列表,当路径长度等于输入数组长度时保存结果。关键点包括:递归出口判断、数字状态标记与恢复、回溯时的剪枝处理。代码实现采用全局变量记录结果和路径,通过深度优先搜索(DFS)遍历所有可能排列,最终返回符合条件的二维结果数组。
一、题目解析
题目给出几个数字,然后要求所有不重复的排列,返回一个二维数组。

二、算法原理 + 代码实现
穷举法
我们可以像"两数之和"这道题目一样,用三层循环来解决,一层循环对应一个数字。
但是,这样的方法效率太低,当数越多,可能会超时。
递归法
我们可以这样做:想象有三个位置,将例1中的三个数字【1,2,3】分别放入。
- 当第一个位置是1时,第二个位置可以是2也可以是3,当第二个位置是2时,第三个位置只能是3,而当第二个位置是3时,第三个位置只能是2;
- 当第一个位置是2时,第二个位置可以是1也可以是3,当第二个位置是1时,第三个位置只能是3,而当第二个位置是3时,第三个位置只能是1;
- 当第一个位置是3时,第二个位置可以是1也可以是2,当第二个位置是1时,第三个位置只能是2,而当第二个位置是2时,第三个位置只能是1;
那照这个逻辑,我们可以画出决策树:

最终得出结果:[[1,2,3],[1,3,2],[2,1,3], [2,3,1],[3,1,2],[3,2,1]]
可以采用递归实现:
- 每一次的递归操作都是判断数字是否已被选
- 第一次递归,判断1被选没,没有则放入第一个位置,然后递归
- 第二次递归,判断到1已被选,不做任何操作,判断2有没有被选,没有则放入第二个位置,然后递归
- 第三次递归,判断到1和2都已被选,判断3有没有被选,没有则放入第三个位置,此时组成的数字(123)已经和所给数组长度一致,就更新结果然后回溯,在回溯的时候恢复现场。
- 回溯到 12__ 位置时,由于和3组合的情况已经有了且只能有这一种情况,再回溯
- 回溯到 1__ __ 位置时,此时第二个位置为2的情况已经全部遍历了,再递归第二个位置为3的情况
- 从 13__ 位置开始,判断到1和3都已被选,此时只剩一种情况,判断2有没有被选,没有则放入第三个位置,此时组成的数字(132)已经和所给数组长度一致,就更新结果然后回溯,在回溯的时候恢复现场。
- 回溯到 13__ 位置时已没有剩余情况,继续回溯到 1__ __ 位置,这时候第一个位置为1的所有情况已经列举完了,再回溯,重新选择第一个位置
- 回到 __ __ __ 位置,此时选择2作为第一个位置,即 2 __ __,然后递归列举第一个位置为2的所有情况
- 递归后,按顺序判断,得知1还未被选择,于是第二个位置选1,得到 21 __ ,然后递归列举所有第一个位置为2、第二个位置为1的情况
- 递归后,按顺序判断,得知1和2都被选择了,且3还未被选择,于是第三个位置选择3,得到 213,此时长度已与所给数组长度一致,开始回溯
- 回溯到 21 __ 发现没有剩余情况,再回溯到 2 __ __ ,判断3还未被选择,于是第二个位置选择3,得到23 __,递归该组合的所有情况
- 递归后发现只有1还未被选择,得到 231,此时长度达到回溯条件,开始回溯
- 回溯到 __ __ __ 位置,然后选择3作为第一个位置继续递归列举所有情况,得到 312 和 321
- 回溯到 __ __ __ 位置,此时所有元素已被列举完毕,退出程序
得到结果:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
全局变量
我们采用递归实现,肯定要涉及到全局变量的,首先需要一个记录结果的 ret(二维数组),然后每一层递归的路径也要记录下来,用 path(是一个一维数组)。
然后由于我们需要判断数字是否已被选,就要有标记数字状态的东西,我们可以使用一个布尔类型的数组,设置它的长度和题目所给数组(nums)的长度一致,通过下标对应nums中的数字,以此来标记数字的状态(true 表示数字已被选择,false 则未被选择)。
dfs 函数
函数头
接下来我们设计 dfs 函数,先搞清楚每一次递归中,要做的事情是什么,也就是重复的事情------那就是判断数字是否已被选择,那数字在哪里呢?在题目所给的数组nums里,于是我们的参数设置只需要一个参数(int[] nums)即可。
函数体
在我们要重复做的事情中,应该具体做点啥呢?如何判断数字是否已被选择?
因为我们是要按照数组的顺序选择数字的,而且选择数字的同时还要更新对应的状态,且都是通过下标来访问的,所以可以使用循环:拿到一个数字,先判断它是否已被选择了,如果未被选择那就把它添加到 path 里头,然后通过设置布尔数组更新数字的状态为 true,表示该数字已被选择,接着递归该组合下的所有情况。
细节问题
回溯
当我们找到一种结果,往上层回溯的时候,涉及到恢复现场的处理。
例如 当我们从 123 开始回溯,此时回溯到 1 __ __ 位置,然后将第二个位置设置为3再接着递归这个组合的所有情况,但是 path 中的中还未改过来呢,此时它的值还是(1,2,3),我们必须找个时机将它后面的元素删掉,回溯一层就删去一个,而且还要重置数字的状态为 false,所以可以将恢复现场操作放到递归回来之后进行(注意是删除最后面的那个元素噢)。
剪枝
我们这棵决策树其实是有剪枝的,那些画了红叉叉的就是剪枝掉了的,那我们已经通过 " 标记数字的状态 + 判断数字是否已被选择 " 来实现剪枝了:当判断到数字的状态是 false 时才选择,若为 true 就不选择(剪枝)。
递归出口
当我们的 path 的长度与题目所给数组 nums 的长度一致,就可以认为是一种结果,把它添加到 ret 中就可以回溯了。
代码实现
Java
class Solution {
List<List<Integer>> ret = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] check;
public List<List<Integer>> permute(int[] nums) {
check = new boolean[nums.length];
dfs(nums);
return ret;
}
void dfs(int[] nums) {
// 递归出口:path和 nums数组长度相等时,更新结果
if (path.size() == nums.length) {
ret.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (check[i] == false) {
path.add(nums[i]);
check[i] = true;
// 递归
dfs(nums);
// 回溯,恢复现场
path.remove(path.size() - 1);
check[i] = false;
}
}
}
}
若有不对请尽管指出😊🌹
完