一看就懂的深度优先搜索和广度优先搜索

什么是搜索算法

算法是作用于具有数据结构之上的,大部分搜索算法是基于“图”这种数据结构。这是因为图的表达能力很强,大部分涉及搜索的场景可以抽象为“图”。

所谓"搜索",最直接的理解就是,从图中寻找一个顶点出发到另一个顶点的路径。针对不同的需求和场景,对应有不同的算法。

图有两种主要存储方式:邻接表和邻接矩阵。这次我们用邻接表来存储图,用无向图来举例说明。

public class Graph{
   //无向图
   private int v;   //顶点个数
   private LinkedList<Integer> adj[];   //邻接表
  
   public Graph(int v){
      this.v = v;
      adj = new LinkedList[v];
      for(int i = 0;i < v;i++){
         adj[i] = new LinkedList<>();
      }
   }
  
   public void addEdge(int s,int t){
     adj[s].add(t);
     adj[t].add(s);
   }
}

广度优先搜索

广度优先搜索(Breadth First Search,BFS),从直观上说,它其实是一种地毯式层层推进的搜索策略,首先查找离起始顶点s最近的,然后是次近的,依次往外搜索,直到找到终止顶点t。实际上,通过广度优先搜索找到的源点到终点的路径也是顶点s到顶点t的最短路径。

尽管广度优先搜索的原理非常简单,但代码实现相对来讲并不简单。先来看看如何实现广度优先搜索。下面看看代码,其中s表示起始顶点编号,t表示终止顶点编号。我们搜索一条从s到t的路径。

public void bfs(int s,int t){
        if(s == t) return;
        boolean[] visited = new boolean[v];
        visited[s] = true;
        Queue<Integer> queue = new LinkedList<>();
        queue.add(s);
        int[] prev = new int[v];
        for(int i = 0;i < v;i++){
            prev[i] = -1;
        }
        while(queue.size() != 0) {
            int w = queue.poll();
            for (int i = 0; i < adj[w].size; i++) {
                int q = adj[w].get(i);
                if (!visited[q]) {
                    prev[q] = w;
                    if(q == t){
                        print(prev,s,t);
                        return;;
                    }
                    visited[q] = true;
                    queue.add(q);
                }
            }
        }
    }
    
    private void print(int[] prev,int s,int t){
        if(prev[t] != -1 && t != s){
            print(prev,s,prev[t]);
        }
        System.out.println(t + "");
    }

广度优先搜索代码实现包含三个重要的辅助变量:visited,queue,prev.

visited数组用来记录已经被访问的顶点,避免顶点被重读访问。如果顶点q已经被访问,那么visited[q]就会被设置为true。

queue是一个队列,队列具有先进先出的特点,很多按层遍历的需求会用到队列。因为广度优先搜索会逐层访问顶点,所以我们只有把第k层顶点都访问完全之后,才能访问第k+1层顶点。当我们访问到底k层顶点时,需要把第k层顶点记录下来,稍后才能通过第k层顶点找到第k+1层顶点。

prev用来记录搜索路径,当从顶点搜索到顶点t后,prev数组中存储从顶点s到顶点t所经历的路径。不过,这个路径时反向存储的。prev[w]存储的是顶点w的前驱节点(也就是路径中的前一个顶点)。我们需要递归来输出正向路径,也就是代码中的print()函数实现的功能。

掌握了广优先搜索算法的原理,我们来看下,广度优先搜索的时间、空间复杂度是多少呢?
最坏情况下,终止顶点 t 离起始顶点 s 很远,需要遍历完整个图才能找到。这个时候,每 个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 O(V+E),其中,V 表示顶点的个数,E 表示边的个数。当然,对于一个连通图来说,也就 是说一个图中的所有顶点都是连通的,E 肯定要大于等于 V-1,所以,广度优先搜索的时间 复杂度也可以简写O(E)。
广度优先搜索的空间消耗主要在几个辅助变量 visited 数组、queue 队列、prev 数组上。 这三个存
储空间的大小都不会超过顶点的个数,所以空间复杂度是 O(V)

深度优先搜索(DFS

深度优先搜索(Depth-First-Search),简称 DFS。最直观的例子就是“走迷宫”。
假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发 现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。 这种走法就是一种深度优先搜索策略。
走迷宫的例子很容易能看懂,我们现在再来看下,如何在图中应用深度优先搜索,来找某个 顶点到另一个顶点的路径。你可以看我画的这幅图。搜索的起始顶点是 s,终止顶点是 t,我们希望在图中寻找一条从 顶点 s 到顶点 t 的路径。如果映射到迷宫那个例子,s 就是你起始所在的位置,t 就是出 口。
我用深度递归算法,把整个搜索的路径标记出来了。这里面实线箭头表示遍历,虚线箭头表 示回退。从图中我们可以看出,深度优先搜索找出来的路径,并不是顶点 s 到顶点 t 的最 短路径

实际上,深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过 程,非常适合用递归来实现。回溯思想我们后面会有专门的一节来讲,我们现在还回到深度 优先搜索算法上。
我把上面的过程用递归来翻译出来,就是下面这个样子。我们发现,深度优先搜索代码实现 也用到了 prev、visited 变量以及 print() 函数,它们跟广度优先搜索代码实现里的作用是 一样的。不过,深度优先搜索代码实现里,有个比较特殊的变量 found,它的作用是,当 我们已经找到终止顶点 t 之后,我们就不再递归地继续查找了。
boolean found = false;
    public void dfs(int s,int t){
        found = false;
        boolean[] visited = new boolean[v];
        int[] prev = new int[v];
        for(int i = 0;i < v;i++){
            prev[i] = -1;
        }
        recurDfs(s,t,visited,prev);
        print(prev,s,t);
    }
    private void recurDfs(int w,int t,boolean[] visited,int[] prev){
        if(found == true) return;
        visited[w] = true;
        if(w == t){
            found = true;
            return;
        }
        for(int i = 0;i < adj[w].size();i++){
            int q = adj[w].get(i);
            if(!visited[q]){
                prev[q] = w;
                recurDfs(q,t,visited,prev);
            }
        }
    }
理解了深度优先搜索算法之后,我们来看,深度度优先搜索的时、空间间复杂度是多少呢?
从我前面画的图可以看出,每条边最多会被访问两次,一次是遍历,一次是回退。所以,图 上的深度优先搜索算法的时间复杂度是 O(E),E 表示边的个数。 深度优先搜索算法的消耗内存主要是 visited、prev 数组和递归调用栈。visited、prev 数 组的大小跟顶点的个数 V 成正比,递归调用栈的最大深度不会超过顶点的个数,所以总的 空间复杂度就是 O(V)。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值