力扣:“金典”回溯:全排列

534 阅读5分钟

前言

对于一些力扣题目,比如这次的全排列,或者是以前讲过的组合问题等,回溯方法都对它们有奇效,具有起死回生,盘活全局的作用,可谓是力扣之旅的必备良方!下面我来给你介绍介绍灵丹妙药————“回溯”

src=http---nimg.ws.126.net--url=http---dingyue.ws.126.net-2021-0928-f5f06ba6j00r04ykc000td2008r00crg008r00cr.jpg&thumbnail=650x2147483647&quality=80&type=jpg&refer=http---nimg.ws.126.jpg

这是开出的回溯药方:

回溯模板

function backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }



接下来我们来“看病”

题目

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

思路分析

这道题目的目的很明显,就是需要找到【1,2,3】的不重复的排列组合,注意是不要重复的。

做此类回溯题时,一般需要用到辅助空间,栈或者队列,比如我们代码当中用到的track是用来辅助暂时存储数据的栈,我们用图来看看自己用栈找结果的逻辑:

第一次栈的显示结果

3b29fc917530c577dd0955fd3e49d91b_1.jpg

第二次栈的显示结果

2.jpg

以上的图解还仅仅是一颗子树的解决方式,还有其他的两颗子树的路径也是相似,这里就不一一画出,我们来分析:

探索

在思考此类问题前呢,需要比较清晰的思维,这种回溯问题实际上“层层嵌套”的过程。我们需要想象出这种排列是一种怎么样的过程:根据我们自己想法所描绘出来的蓝图:

  • 首先是让1进栈?2进栈?还是3进栈呢?,这三种情况都有,我们需要分别讨论,这个时候按顺序先让1进栈;
  • 之后,让2 ,3进栈都行,那么此时又是两种情况,此时我们暂时让2进栈,
  • 最后3进栈,那么此时这条路就走完了,因为接下来就没有数字了。

这就是回溯当中的深度优先遍历思想,“不撞南墙不回头”,一直将一条路进行到底,深度优先探索解二叉树,直到无路可走或者是找到了目标答案,否则不停下来,这是回溯当中很重要的思想。 在最后,我们就把找到的结果压进结果数组,返回即可。

回退

有的人就说了,我们第一次1,2,3的情况还没有分析完呢,这怎么整? 嘿嘿,别急慢慢来。

我们在上述情况当中已经找到了一组目标结果,但是如何讨论其他的情况呢?这个时候我们有需要了解回溯另外一种思想: 回退。简单来说就是回到上一步继续讨论原来未讨论完的情况。在回溯问题当中,这步骤很简单,就是track.pop(),是的你没有看错,就是出栈操作。这是怎么做到的呢?

比如我们找到了一组结果,【1,2,3】,那么我们执行回退操作,直到还在【1】已经入栈,但是在2,3选择的这个时候,我们一开始是选择了2进栈,所以此时我们改变策略,让3进栈,那么此时又找到了一组结果,这个时候再次执行回退操作,直到我们最一开始的1,2,3入栈选择的时候,我们再让 2 入栈即可,重复步骤,最后得出结果。

不过怎么模拟这个选择的过程呢?那就是循环。而且需要注意的是,这个地方要做去重操作。

虽然回溯的代码量较少,但是实际上你们也了解这个过程,空间上比较复杂,是典型的用空间换取时间的一种算法,但是依然改变不了他的适用性,仍然是力扣必备方法!

具体代码

var permute = function(nums) {
    const result = []
    backTrack(nums, result, [])
    return result
};

function backTrack (nums, result, track) {
    if (track.length === nums.length) {
        // 更改引用类型的指针。目的;防止回溯行为影响到当前数组状态。
        result.push([...track])
        return
    }
    for (let i = 0; i < nums.length; i++) {
        if (track.includes(nums[i])) {
            continue
        }
        track.push(nums[i])
        backTrack(nums, result, track)
        track.pop()
    }
}

代码分析

在这里需要定义我们的结果数组result[],这是用来存放最终结果的空间。而backTrack函数就是我们的核心代码,其中套用了回溯模板,自行比对后你们可以发现,确实没啥两样。但是有一些细节需要注意,就是数组是否需要去重,比如这里,我们就使用了track.includes(nums[i])的方法来跳过已经做出过选择的数字。

总结

总的来说,刷题有方法,我们需要根据做出的题目来总结出方法,不是说刷题多就有用,刷题只不过是给你们给予总结方法的经验,而不是给我们刷更多题目的动力,方向就走错了,所以说还是要多学习,多总结。

我是小白,我们一起力扣!