一,什么是搜索算法

算法是基于特定数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。

树是图的一种特例(连通无环的图就是树)。

图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。具体方法有很多,两种最简单、最“暴力”的深度优先、广度优先搜索,还有 A*IDA* 等启发式搜索算法。深度优先搜索算法和广度优先搜索算法,既可以用在无向图,也可以用在有向图上。

图(采用邻接表存储)的 C++ 代码实现如下:


#include <list>
// 无向图结构的定义
class Graph{
private:
    int v;  // 顶点个数
    list<int> adj()  // 存储的邻接表
public:
    // 构造函数定义
    Graph(int v){
        adj = new List<int>(v);
        for (int i = 0; i < v; i++){
            adj[i] = new list<int> ();
        }
    }
    // 无向图一条边存储 2 次
    void addEdge(int s, int t){
        adj[s].push_back(t);
        adj[t].push_back(s);
    }
}

二,广度优先搜索(BFS)

广度优先搜索(Breadth-First-Search),我们平常都简称 BFS。直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。

求图中起始顶点 s 到终止顶点 t 的最短路径,可使用 bfs 算法,代码如下:

#include <queue>
using namespace std;
void bfs(int s, int t){
    if( s==t ) return;
    bool visited[v];  // 用来记录已经访问过的顶点, v 表示顶点个数
    queue<int> queue; // 一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。
    queue.push_back(s); // 添加起始顶点
    vector<int> prev(v, -1); // prev 用来记录搜索路径, prev[w]存储的是,顶点 w 是从哪个前驱顶点遍历过来的。
    while(queue.size() != 0){
        int w = queue.pop();
        for(int i= 0; i<adj[w].size();++i){
            int q = adj[w].at(i);
            if(!visited[q]){
                prev[q] = w;
                if(q==t){
                    print_map(prev, s, t);
                    return;
                }
                visited[q] = true;
                queue.add(q);
            }
        }
    }

void print_map(int prev[], int s, int t){
    if(prev[t] != -1 && t!=s){
        print_map(prev, s, prev[t]);
    }
    cout << t << "+ ";
}

queue 是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第 k 层的顶点都访问完成之后,才能访问第 k+1 层的顶点。当我们访问到第 k 层的顶点的时候,我们需要把第 k 层的顶点记录下来,稍后才能通过第 k 层的顶点来找第 k+1 层的顶点。所以,我们用这个队列来实现记录的功能。

prev 用来记录搜索路径。当我们从顶点 s 开始,广度优先搜索到顶点 t 后,prev 数组中存储的就是搜索的路径。不过,这个路径是反向存储的。prev[w]存储的是,顶点 w 是从哪个前驱顶点遍历过来的。比如,我们通过顶点 2 的邻接表访问到顶点 3,那 prev[3]就等于 2。为了正向打印出路径,我们需要递归地来打印。

最坏情况下,终止顶点 t 离起始顶点 s 很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 $O(V+E)$,其中,V 表示顶点的个数,E 表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,E 肯定要大于等于 V-1,所以,广度优先搜索的时间复杂度也可以简写为 $O(E)$。

广度优先搜索的空间消耗主要在几个辅助变量 visited 数组、queue 队列、prev 数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是 $O(V)$。

三,深度优先搜索(DFS)

深度优先搜索(Depth-First-Search),简称 DFS

最直观的例子就是“走迷宫”。假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。这种走法就是一种深度优先搜索策略。

bool found = false;  // 类成员变量

void dfs(int s, int t){
    found = false;
    bool visited[v];  // v 表示顶点个数
    int prev[v];
    for(int i=0; i< v;i++){
        prev[i] = -1;
    }
    recurDFS(s, t, visited, prev);
    print_map(prev, s, t)
}
void recurDFS(int w, int t, bool 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].at(i);
        if(!visited[q]){
            prev[q] = w;
            recurDFS(q, t, visited, prev);
        }
    }
}

四,内容总结

广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 $O(E)$,空间复杂度是 $O(V)$。

参考资料

《数据结构与算法之美》-深度和广度优先搜索

Copyright © zhg5200211@outlook.com 2022 all right reserved,powered by Gitbook该文章修订时间: 2022-12-03 05:38:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""