【递归算法】全排列

题目链接

文章摘要:

  • 本文解析了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;
            }
        }
    }
}

若有不对请尽管指出😊🌹

相关推荐
dapeng28702 小时前
C++与Docker集成开发
开发语言·c++·算法
2501_945423542 小时前
C++中的策略模式实战
开发语言·c++·算法
2301_792308252 小时前
C++与自动驾驶系统
开发语言·c++·算法
会编程的土豆2 小时前
【数据结构与算法】LCS刷题
数据结构·算法·动态规划
无敌憨憨大王2 小时前
最小生成树
算法
Jasmine_llq2 小时前
《B4258 [GESP202503 一级] 四舍五入》
数据结构·算法·整数运算实现四舍五入整十数算法·批量输入遍历算法·逐行输出算法·整数算术运算组合算法·顺序输入处理算法
2401_874732532 小时前
模板编译期排序算法
开发语言·c++·算法
阿Y加油吧2 小时前
力扣打卡——螺旋矩阵、旋转图像
leetcode
weixin_421922692 小时前
C++与Node.js集成
开发语言·c++·算法