前言

本文旨在帮助小白熟悉DFS的基本流程和递归算法的理解,主要包括我们用回溯的思想对题目进行分析的过程,编写代码过程中递归函数语义以及参数的确定等一系列细节。

基本的概念

官方解释:一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。

其实说白了就跟咱们闯关游戏一样,遇到分岔路然后我们选一条路走到头结果发现不是终点,我们则需要重新返回到分岔路进行重新选择。

从上面我们可以提炼出来基本模板

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择


路径:你已经选择的道路。
选择列表:岔路口可选择的道路。
做选择:在岔路口做道路的选择。
撤销操作:发现此路不通,返回最近的岔路口,选其它的道路。

代码模块不唯一,选择自己可以理解的即可,上面的代码模板主要借鉴labuladong的文章

下面咱们就以全排列、组合为例,一起来深入理解DFS算法和递归算法。

全排列问题

 问题分析:

1、首先要搞清题目,知道全排列是什么意思。

2、根据题目我们可以画出相应的树结构

注意:画图可以更加清晰的捋清我们的思路,如果直接跳入递归很容易混淆。

 接下来我们试着带入一下模板里面:1、如果先选择路径为[1] ,

                                                           2、剩余可选择的有[2,3],

                                                           3、继续选择下一条道路,并记录已走过的路径[1,2]

                                                           4、如果不需要选择,则直接添加[3]。返回路径[1,2,3]

                                                           5、返回到最近的岔路口,选择其它道路重复上述过程

 3、确定递归函数的语义以及参数

这一步对初学者来说应该是比较有难度的,对递归函数语义的掌握不是一蹴而就的,需要经验的积累才能熟练。一般递归函数的语义往往都是来根据题目要求决定的。本题要求我们求出不重复的全排列组合,所以我们的语义就为:给我一组数,返回一个没有重复数字的排列数组。

还有人对参数的选择比较迷茫,一句话变量就是会变的量。就本题来说会变的量无非就是可选择的路径记录的路径

4、代码分析

class Solution {
    List<List<Integer>>res =new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
       LinkedList<Integer>track=new LinkedList<>(); //记录路径
       backtrack(nums,track);
       return res;
    }
    void backtrack(int []nums,LinkedList<Integer>track)
    {
        if(track.size()==nums.length)  //结束条件,所有数字都排列完毕
        {
            res.add(new LinkedList(track));
            return;
        }
        for(int i=0;i<nums.length;i++) //选择列表
        {
            if(track.contains(nums[i]))  //剪枝,有重复数字直接跳过根据题目来决定
            {
                continue;
            }
            track.add(nums[i]);  //做选择 添加路径
            backtrack(nums,track); //递归进入下一个岔路口
            track.removeLast(); // 撤销并回到最近的岔路口
        }
    }
}

初学者建议先把树状图和代码对应起来,自己画一下递归的过程,帮助自己理解!!!

组合问题

 问题分析:

1、搞清题目要求,我们可以发现同一个组合内数字不可以重复,组合之间不能重复(可以为剪枝提供思路)

2、画图 

通过图我们可以发现,因为题目给的是非递减的数组,所以我们的选择列表从当前数的后一位开始的方式,进行剪枝操作,解决了题目要求。

 3、确定递归函数的语义以及参数

语义:给我一组数和限制长度K,返回一个没有重复数字且长度为K的排列数组。

参数:[1-n]的数,限制长度K,实现选择列表从当前数后一位的标记start,存储结果的数组

4、代码分析

class Solution {
        List<List<Integer>>res=new  LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
       if(k<=0||n<=0)//特殊情况直接结束
       {
           return res;
       }
       LinkedList<Integer>track=new  LinkedList<>();
       backtrack(n,k,1,track);
       return res;
    }
    void backtrack(int n,int k,int start, LinkedList<Integer>track) 
    {
        if(k==track.size())//结束条件,选择了k个不重复的数
        {
            res.add(new LinkedList(track)); //返回结果
            return;
        }
        for(int i=start;i<=n;i++) //剪枝操作,每次从start后开始选择,避免数字和组合重复问题
        {
            track.add(i);  //做选择
            backtrack(n,k,i+1,track); //递归进入下一个岔路口
            track.removeLast(); //撤销,返回最近的岔路口
        }
    }
}

总结:

DFS算法和递归都是比较抽象的,一定要先画图理解,直接手撕代码很容易把自己劝退的。递归函数的语义和参数选择部不熟的,可以看看别人的优秀代码,学习人家的思路。

---------------------------------------------------------------------------------------------------------------------------------

 上面是一道很典型的回溯问题,大家可以上手试一试,在评论区留下自己的递归函数

更多推荐

DFS深度优先搜索算法(适合初学者)