数据结构与算法之美(笔记16)深度和广度优先搜索

什么是“搜索”算法?

图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。具体的方法有很多,比如深度优先,广度优先搜索,还有A*,IDA*等启发式搜索算法。

广度优先搜索(bfs)

直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。

原理虽然看起来简单,但是实现起来却比较难。

先给出代码,再解释:

    void bfs(int s,int t){
        if(s == t) return;
        /*定义三个辅助变量,visited用来记录已经被访问的顶点。
         * Q是一个队列,用来储存已经被访问、但相连的顶点还没有被访问的顶点。
         * prev用来记录搜索路径,不过这个路径是反向储存的,prev[w]储存的是,顶点w是从哪个前驱结点遍历过来的。
         */
        bool visited[max_v] = {0};
        queue Q(max_v);
        int prev[max_v] = {0};
        for(int i=0;i<max_v;++i){
            prev[i] = -1;
        }

        visited[s] = true;
        Q.enqueue(s);

        while(Q.size() != 0){
            int cur = Q.dequeue();
            for(size_t i=0;i<adj[cur].size();++i){
                int next = adj[cur][i];
                if(!visited[next]){
                    prev[next] = cur;
                    if(next == t){
                        print_bfs(prev,s,t);
                        return;
                    }
                    visited[next] = true;
                    Q.enqueue(next);
                }
            }
        }
    }

    void print_bfs(int* prev,int s,int t){
        if(t == s || prev[t] == -1){
            cout << s << "->";
            return;
        }
        print_bfs(prev,s,prev[t]);
        cout << t << " ->";
    }

 我们只要能够理解visited数组,Q队列,prev数组,就能够理解这个实现了。

visited是用来记录已经被访问的顶点,用来避免顶点被重复访问。如果顶点q被访问,那相应visited[q]会被设置为true。

Q是一个队列,用来存储已经被访问,但相邻的顶点还没有被访问的顶点。我们只有把第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数组,Q队列,prev数组上。这三个储存空间的大小都不会超过顶点的个数,所以空间复杂度是O(V)。

深度优先搜索

深度优先搜索,简称DFS。最直观的例子就是“走迷宫”。

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

我们用深度递归算法,把整个搜索的路径标记出来了。这里面实线箭头表示遍历,虚线箭头表示回退。从图中我们知道,深度优先搜索找出来的路径,并不是顶点s到顶点t的最短路径。

这里给出代码的实现:

    bool found;// 全局变量或者类成员变量
    void dfs(int s,int t){
        found = false;
        bool visited[max_v] = {0};
        int prev[max_v] = {0};
        for(int i=0;i<max_v;++i){
            prev[i] = -1;
        }
        recurdfs(s,t,visited,prev);
        print_bfs(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][i];
            if(!visited[q]){
                prev[q] = w;
                recurdfs(q,t,visited,prev);
            }
        }
    }

深度优先搜索代码实现也用到了prev,visited变量以及print函数,它们跟广度优先搜索代码实现里的作用是一样的。不过,它有个比较特殊的变量found,它的作用是,当我们已经找到终止顶点t之后,我们就不再递归地继续查找了。

那么时间或者空间复杂度呢?

深度优先搜索算法,每条边最多会被访问两次,一次是遍历,一次是回退。所以DFS算法的时间复杂度是O(E),E表示边的个数。深度优先搜索算法的消耗主要是visited,prev数组和递归调用栈。它们都与顶点的个数有关,所以空间复杂度是O(V)。

猜你喜欢

转载自blog.csdn.net/weixin_42073553/article/details/88902094