美文网首页
图 - 深度/广度 优先搜索,两种暴力搜索算法(遍历算法)

图 - 深度/广度 优先搜索,两种暴力搜索算法(遍历算法)

作者: 天命_风流 | 来源:发表于2020-03-27 16:05 被阅读0次

图是一种表达能力很强的非线性数据结构,因为要表达很多信息,所以他的实现就要比其它的线性数据结构复杂。相应的,图的搜索(查询)就不像之前的线性结构那样简单了,今天在这里我们介绍两种最简单也是最暴力的图的搜索算法:深度优先搜索 和 广度优先搜索

注:当你的目的不是查找某个数据,而是获取图中所有的数据的时候,可以将这两种搜索算法改造成图的遍历算法,图的遍历也有很多用处,例如使用网络爬虫获取整个互联网的信息。

对于图的搜索算法,最直接的理解就是:在图中从一个顶点出发,找到到达另一个顶点的路径

图的构建

图的搜索算法可以应用在有向图、无向图中,本文使用无向图作为例子讲解,下面是构建一个图的代码:

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)

广度优先搜索是一种“地毯式”层层推进的搜索策略,它先查找起始顶点最近的顶点,然后是次近的,依次往外搜索,直到找到期待的数值: 广度优先搜索.jpg

下面是广度优先搜索的代码:

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) { // 递归打印s->t的路径
  if (prev[t] != -1 && t != s) {
    print(prev, s, prev[t]);
  }
  System.out.print(t + " ");
}

在搜索算法中,有三个非常重要的辅助变量,理解这三个变量,基本上就能读懂这段代码了:

  • visited:记录已经被访问的顶点,用来避免顶点被重复访问。如果顶点 q 被访问,那相应的 visited[q] 会被设置为 true。
  • queue:一个队列,用于存储已经被访问,但是还没有被访问过的相连的顶点。广度优先搜索算法是逐层访问的,所以队列中的元素是按层有序的。
  • prev:用来记录搜索路径。当我们从 s 开始,搜索到顶点 t 后,使用 prev 数组中的数据构建搜索路径。比如,我们通过顶点 x 访问到了顶点 y ,那我们就将 prev[y] 设置为 x 。由于 prev 是反向构建的,所以我们需要特殊处理才能打印正确的路径信息,你需要看一下 print( ) 函数的实现方式。
下面有一个广度优先搜索过程的分解图,可以帮助你理解代码,特别是三个辅助变量的用途: 广度优先实例1.jpg
广度优先实例2.jpg
广度优先实例3.jpg

复杂度分析

  • 时间复杂度:在最坏情况下,算法要遍历所有的顶点和边,也就是 O(V+E),其中 V 是顶点的个数,E 是边的个数。如果一个图是连通图,那么 E >= V-1 ,所以,它是时间复杂度也可以简写为 O(E)。
  • 空间复杂度:我们需要三个辅助变量辅助计算,而这三个辅助变量使用的空间都不会超过 V,所以空间复杂度为 O(V)。

深度优先搜索(Depth First Search)

深度优先搜索最直观的例子就是走迷宫。
假设你在迷宫的某个岔路口,你不知道走哪条路,那你就随意选择一条路,如果这条路是一条死路,就回退到岔口,换一条路继续走,直到找到出口。

这就是一种典型的深度优先搜索策略: 深度优先搜索.jpg
深度优先搜索使用了一个著名的算法思想:回溯思想。这种思想解决问题的过程,非常适合使用递归来实现。

深度优先算法中也使用到了 prev、visted 两个变量以及 print( ) 函数。同时它还使用了一个 found 变量,用于标记是否找到终点 t ,如果 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 是边的个数。
  • 空间复杂度:我们需要递归调用栈 和 两个辅助变量,它们的规模不会超过顶点的个数 V ,所以空间复杂度为 O(V)。

总结

  • 深度优先搜索 和 广度优先搜索 是两种非常基础也是非常暴力的搜索算法。
  • 他们的时间复杂度都是 O(E),空间复杂度都是 O(V)。
  • 广度优先搜索 是一种地毯式的搜索算法。深度优先搜索 利用了回溯思想和递归技巧。
  • 思考,我们能否通过广度优先搜索算法实现QQ中推荐“可能认识的人”的功能?

下面是我自己写的图的代码,可以供你参考

import queue

class Graph:
    def __init__(self, n):
        self.v = [[] for i in range(n)]

    def addEdge(self, x, y):
        '''
        建立一条边
        :param x:边的一个顶点
        :param y: 边的另一个顶点
        :return: None
        '''
        self.v[x].append(y)
        self.v[y].append(x)

    def __repr__(self):
        return str(self.v)

    def dfs(self, s, e):
        '''
        深度优先搜索
        :param s:起始位置
        :param e:结束位置
        :return:None
        '''

        visted = [False for i in range(len(self.v))]  # 记录每个顶点是否被浏览过
        path = [-1 for i in range(len(self.v))]  # 记录路径信息
        path[s] = s
        r = self.recueDfs(s, e, s, visted, path)  # 放入递归程序中

        print('')
        return r

    def recueDfs(self, s, e, cur, visted, path):
        '''
        深度优先搜索会在这里进行递归
        :param s: 起始位置
        :param e: 结束位置
        :param cur: 当前位置
        :param visted: 记录顶点是否被浏览过
        :param path: 记录路径信息
        :return: 寻找完成的路径信息
        '''
        visted[cur] = True

        if cur == e:  # 如果当前顶点是终止顶点
            self.print_path(path, s, e)  # 打印路径
            return path

        for i in self.v[cur]:  # 如不是终点,依次递归该顶点的每个相邻顶点
            if visted[i]:
                continue
            path[i] = cur
            res = self.recueDfs(s, e, i, visted, path)
            # 如果上一层递归有返回值,代表找到了路径,可以直接 return 跳出;如果递归没有返回值,证明没有找到路径,退出递归函数相当于回退
            if res:
                return res

    def bfs(self, s, e):
        '''
        广度优先搜索算法
        :param s: 起始位置
        :param e: 结束位置
        :return:寻找完成的路径信息
        '''
        visted = [False for i in range(len(self.v))]  # 记录每个顶点是否被浏览过
        visted[s] = True
        q = queue.Queue(len(self.v))  # 维护一个队列,用于放置之后要遍历的顶点
        q.put(s)
        path = [-1 for i in range(len(self.v))]  # 记录路径信息
        path[s] = s
        while q.qsize() != 0:  # 队列
            w = q.get()
            for i in self.v[w]:
                if not visted[i]:
                    path[i] = w
                    visted[i] = True
                    q.put(i)
                    if i == e:
                        self.print_path(path, s, e)
                        print('')
                        return path

    def print_path(self, path, s, e):
        '''
        输出路径信息
        :param path: 记录路径信息的数组
        :param s: 起始位置
        :param e: 结束位置
        :return: None
        '''
        if e == s:
            print(e, end='')
            return
        else:
            self.print_path(path, s, path[e])
            print('->' + str(e), end='')


if __name__ == '__main__':
    g = Graph(6)
    g.addEdge(1, 0)
    g.addEdge(1, 2)
    g.addEdge(1, 3)
    g.addEdge(1, 5)
    g.addEdge(2, 5)
    g.addEdge(3, 5)
    g.addEdge(4, 5)
    res = g.bfs(0, 4)
    print(res)
    res = g.dfs(0, 4)
    print(res)

以上就是两种图的搜索算法。

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我大量引用了其中的代码和截图,如果想要了解具体内容,可以前往极客时间

相关文章

  • 搜索

    一、深度优先搜索 图深度优先遍历、深度优先搜索算法求有权图两点最短路径 二、广度优先搜索 图广度优先遍历、广度优先...

  • 【数据结构】广度优先搜索算法BFS

    对于广度优先遍历算法DFS可以参考前一篇文章【数据结构】深度优先搜索算法DFS 广度优先遍历 广度优先遍历(Bre...

  • 最短路径

    图的两种搜索算法,深度优先搜素和广度优先搜索。这两种算法主要是针对无权图的搜索算法。针对有权图,也就是图中的每条边...

  • 数据结构与算法--BFS&DFS

    “搜索”算法 深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构。 图上的搜索算法就是,在图中找出从一个...

  • 数据结构之图的遍历-深度优先搜索(DFS)和广度优先搜索(BFS

    两种遍历 图的遍历分为深度优先搜索(Depth First Search)和广度优先搜索 深度优先搜索(DFS) ...

  • python 广度优先算法

    文章概述 广度优先搜索算法概述 广度优先搜索算法 广度优先算法是基于树或者图的,当然树也是一种特殊的图,因此我们先...

  • 数据结构与算法--深度和广度优先搜索

    什么是“搜索”算法? 算法是作用于具体数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构...

  • 学习js数据结构与算法7—图

    图 图的遍历 两种算法可以对图进行遍历:==广度优先搜索和深度优先搜索== 当要标注已经访问过的顶点时,我们用三种...

  • 2. 图的遍历算法

    图的遍历算法包括: 1. 深度优先搜索. 2. 广度优先搜索 1. 深度优先搜索 DFS (Depth Firs...

  • “深度优先搜索+剪枝”思维在产品经理面试题中的应用

    1. 什么是深度优先搜索算法 深度优先搜索(Depth-first Search)对应于广度优先搜索(Bread...

网友评论

      本文标题:图 - 深度/广度 优先搜索,两种暴力搜索算法(遍历算法)

      本文链接:https://www.haomeiwen.com/subject/fuwluhtx.html