算法之深度优先搜索

272 阅读7分钟
原文链接: mp.weixin.qq.com

戳上面的蓝字关注我们哦!

本文作者:丹丘生

文末阅读原文获取文中示例代码

一、引言

研究算法,写算法是很枯燥的过程;但是找到规律以及通用之处,会发现它是如此简单,如此妙不可言,前提是需要有耐心。

本人不喜欢一上来就是扒拉扒拉一些概念然后弄个经典例题之后闪人。个人喜欢循序渐进,从小问题引出要用的算法;

深度优先搜索和广度优先搜索是搜索算法里面比较基础的算法,理解以及实现非常容易,当然说归说,如何容易,且看下文。

二、小小问题

Q:1~4 数字的全排;

第一种:一般情况看到这个题目,第一反应就是暴力穷举,4个for循环搞定一切;

第二种:采用深度优先搜索,但是第一反应,怎么可能啊?   第二反应,我不会啊,怎么深度优先搜索呢?很简单,让我们来写最简单的代码

第一种思路代码的V1版本:

package dfs;/** * 暴力全排0~4 * 思路:四个循环,从1开始到4结束.在这里优化了一下,用数组存储,然后用另一个数组存储结果,之后相加个数就可以知道有没有数字重复。 * 因为对于全排,只有1,2,3,4仅且出现1次。 * * @author danqiusheng */public class NormalFullV1 {    public static void main(String[] args) {        int count = 0;// 记录排序次数        int[] arr = new int[5]; // 存储结果        for (arr[1] = 1; arr[1] < 5; arr[1]++) {            for (arr[2] = 1; arr[2] < 5; arr[2]++) {                for (arr[3] = 1; arr[3] < 5; arr[3]++) {                    for (arr[4] = 1; arr[4] < 5; arr[4]++) {                        int[] result = new int[5];// 判断是否重复                        boolean flag = true;                        for (int i = 1; i < 5; i++) {                            result[arr[i]] += 1;                            if(result[arr[i]] > 1) flag = false; // 判断当前数字是否重复出现                        }                        if(flag){ // 所有的数字出现                            System.out.println(arr[1] + "" + arr[2] + "" + arr[3] + "" + arr[4]);                            count++;                        }                    }                }            }        }        System.out.println("total count:"+count);    }}

是不是你想的那样? 简单吧? 让我们来优化一下这优雅的代码,让它更优雅点;

第一种思路代码优化V2:

Q: 基于V1版本的优化,如何优化呢?

想想,现在只是4个数字的全排,用4个循环解决,如果题目改一下,变成9个数字的全排,看下V1版本的代码要做如何的改动呢?

很简单,9个for,重复的内容,不变的循环。最后只需要更改下标,判断以及输出即可满足。如果是二十几个数字全排呢?崩溃了。

在这里,我引入递归。递归的特点,找出重复的计算以及边界条件即可,递归的概念这里不做任何讲解。具体的优化如下:

package dfs;/** * 暴力全排0~4 * V2版本:递归 * 边界点:多少个排序数 * 重复内容:for循环 * * @author danqiusheng */public class NormalFullV2 {    static int[] arr = new int[5];    static int step_total = 5;    static int count = 0;    public static void main(String[] args) {        full(1); // 1开始        System.out.println("total count:" + count);    }    public static void full(int step) {        if (step >= step_total) { // 出口;当下标超出            int[] result = new int[5];// 判断是否重复            boolean flag = true;            for (int i = 1; i < step_total; i++) {                result[arr[i]] += 1;                if (result[arr[i]] > 1) flag = false; // 判断当前数字是否重复出现            }            if (flag) { // 所有的数字出现                System.out.println(arr[1] + "" + arr[2] + "" + arr[3] + "" + arr[4]);                count++;            }            return;        }        // 重复内容        for (int i = 1; i < step_total; i++) {            arr[step] = i; // 第step下标位置 放i            full(step + 1);        }    }}

看,是不是比for循环优雅一点;如果这时候需要二十几个数字全排,只需要将静态数组长度,step_total以及输出修改即可;     

但是问题又来了,这个代码我们能不能再优化一下呢?从刚才的暴力到现在的递归解决,会发现,判断条件那一块的代码是原封不动的照搬过来的;

int[] result = new int[5];// 判断是否重复boolean flag = true;for (int i = 1; i < step_total; i++) {   result[arr[i]] += 1;  if (result[arr[i]] > 1)     flag = false;   // 判断当前数字是否重复出现  }if (flag) {  // 所有的数字出现 System.out.println(arr[1] + "" +   arr[2] + "" + arr[3] + "" + arr[4]); count++;}

这一块代码的主要意思是在全排完毕后判断数字是否重复了,如果重复就不输出;

那问题来了,我们能不能在之前就做好这个判断呢?比如在全排当前数的时候,我们能不能把当前全排的这个数与前面已经全排的数比较判断有没有重复呢?

第三种代码优化V3:

package dfs;/*** 暴力全排0~4* V3版本:递归时判断当前数是否重复* 边界点:多少个排序数* 重复内容:for循环** @author danqiusheng*/public class NormalFullV3 {   static int[] arr = new int[5]; // 存储结果数组   static int[] result = new int[5];// 判断是否重复   static int step_total = 5;   static int count = 0;   public static void main(String[] args) {       full(1); // 1开始       System.out.println("total count:" + count);   }   public static void full(int step) {       if (step >= step_total) { // 出口;当下标超出           System.out.println(arr[1] + "" + arr[2] + "" + arr[3] + "" + arr[4]);           count++;           return;       }       // 重复内容       for (int i = 1; i < step_total; i++) {           if (result[i] == 0) { // 如果当前数没有被全排               result[i] = 1; // 标记当前数已经全排               arr[step] = i; // 第step下标位置 放i               full(step + 1);               result[i] = 0;                // 标记当前数不全排,这一步是因为当前数已经全排完毕              //(Full()这个方法递归已经结束!)              //此时应该取消之前标记这个数的已经全排           }       }   }}

看,代码是不是少了很多呢?此时的V3版还可以怎么优化就看你们自己了,但是我今天的算法已经分享完了,V3版是深度优先搜索的模子,愣住了吗?V3是深度优先搜索? 深度优先搜索到底是什么呢?

三、概念

什么是深度优先搜索呢?深度优先搜索简称DFS,它最重要的两个点,第一个是当前该怎么做以及下一步该如何做。

正如我们的V3版Full方法里面的循环,当前这步放当前数,然后尝试下一步应该放什么。当然了,下一步的操作和当前这步的操作是一样的。 所以写起来很简单。

四、总结

深度优先搜索基于上面的概念,有一个基本的模版,如下:

public static void dfs(int step){  // 判断边界 递归出口  // 遍历每一种可能,进行递归  for(int i = 0;i<n;i++){    //尝试下一步    dfs(step+1);  }}

当然,这只是一个基础的,因为不同的问题,深搜体现的方式不同。但是写了很多算法,大部分都是用for循环来进行尝试下一步要做什么。

五、小试牛刀

六、代码

  • 示 例 代 码 [NormalFullV1] (https://github.com/danqiusheng/algorithm_practice/blob/master/src/dfs/NormalFullV1.java)

  • 示 例 代 码 [NormalFullV2] (https://github.com/danqiusheng/algorithm_practice/blob/master/src/dfs/NormalFullV2.java)

  • 示 例 代 码 [NormalFullV3] (https://github.com/danqiusheng/algorithm_practice/blob/master/src/dfs/NormalFullV3.java)

  • [小试牛刀2](https://github.com/danqiusheng/algorithm_practice/blob/master/src/seven/Project_3.java)

关注算法文摘,修炼开发内功