BFS和DFS
引言
在上节,介绍了图的基本概念,还是举之前的例子,房间布局图:
你想找到一条从Kitchen到Bed2的路径,这时候你必须要沿着某条边的序列走到Bed2,而且不经过任意房间超过一次。对于任意的图,在最坏情况下,编写这样的程序需要走遍整个图。这样的过程就是图的遍历。遍历的过程就是访问图中顶点的过程,在访问一个顶点的时候执行某个动作,动作可简单可复杂(不止是打印,更新,便于扩展)。
有两个大家耳熟能详的图遍历方法——BFS和DFS,正是本文介绍的主要内容。
BFS
BFS即广度优先搜索,其主要特征是它在图中涟漪式地访问顶点。以下图为例:
以A为涟漪起点,第一波访问其每一个邻居,第二波访问未访问过的邻居的邻居顶点,以后每一波依此类推,直到图中所有顶点访问完毕。说白了,依次访问距离起始顶点距离为1,2,3....的顶点。
如何实现?
BFS的实现是不是有点似曾相识?这不就是树的层级遍历的扩展吗?没错!实现起来原理也是一样的,利用队列,在遍历顶点的同时将其未遍历的邻居加入到队列中。一波访问结束,再将队列中的顶点取出,重复之前的操作。用《数据结构从应用到实现Java版》上一张图表示,如下:
这里必须维护一个数据结构,存储已经访问过的顶点信息,每次将顶点加入队列之前,检查顶点是否已被访问,避免重复操作。
鄙人自己也实现了一版,具体代码,见博文末尾。
DFS
DFS即深度优先搜索,沿着图中一条路径尽可能往深走到底,访问这条路径上的每个顶点,当无法前进的时候退回并尝试另个方向,同样尽可能深地前行。说白了就是一条道走到黑,直到无路可走了,再回退寻找下个访问节点,继续一条路走到黑。
还是上例,一张图。
访问顺序如图,需要注意的是,使用DFS的时候,选择哪个顶点为下一个访问顶点依赖于邻居的存储顺序。这个例子中,是以字母表顺序存储邻居,所以遍历顺序如上。同样,也要维护一个数据结构,存储以遍历顶点信息。
DFS的使用可以使用递归,也可以使用栈。如果有同学对栈实现DFS感兴趣的话,不妨看一下,我之前的博文http://www.cnblogs.com/ustcwx/p/7756783.html 。
遍历驱动器
如果所有图都是无向连通图,那无论是BFS还是DFS,只要从任意一个顶点出发就可以遍历所有顶点。但遇到非连通图,或者弱连通有向图,怎么办?以地图为例:
这个图至少要从三个顶点出发,才能遍历结束全部顶点。这时候我们就需要一个驱动器,对每个顶点检查是否已访问,对未访问的顶点调用遍历算法。用伪代码表示,如下:
1 XFSdriver 2 3 for each vertex v in the graph do 4 if(v is not visited) then 5 XFS(v) 6 end if 7 end for
时间复杂度
设图有v个顶点,e条边。无论是BFS还是DFS,遍历过程中,每个顶点有且只被访问一次,其区别仅在于顶点和边的扫描访问顺序不同。但是顶点会被检查若干次,即检查其是否已被访问过。检查多少次呢?访问每个顶点的时候都会检查其邻居,所以检查的时候是按照边的方向检查。无向图中e条边,对应有向图中的2e条边。所以无向图的时间复杂度为O(v+2e),而有向图的时间复杂度为O(v+e)。
如果把驱动器检查的次数也算上,无向图的时间复杂度为O(v+e)(由O(2v+2e)得到),而有向图的时间复杂度为O(2v+e)。
具体实现
其实实现不难,最初我也实现了功能,但总觉得代码有点怪。后来对比了大神的代码,发现还是接口设计得比较烂,设计模式功底不到家。代码后来又改了一版,这里摘抄另鄙人受益良多的两段话。
设计与接口
- 此类必须只做它应该做的事情,不多也不少。例如,深度优先搜索知道这个图,所以,它只负责以深度优先顺序遍历顶点。然而,它对使用它的软件不了解,所以,在访问一个顶点的时,它不能定义要做什么,这要留给这个软件。
- 此类必须富有弹性,必须允许用户有足够多的钩子,在此用户可以控制执行软件特化的动作。作为一个例子,我们知道,在深度优先搜索的前行阶段遇到一个顶点时,要标识这个顶点已见过。但是,看见一个顶点可以与访问这个顶点分开,而不影响深度优先搜索的正确性。
如果不太理解的话,可以接着看后续博文。言归正传,这里我把BFS和DFS写到一个类里面了,不要拍我啊,哈哈。
package com.structures.graph; import com.structures.linear.CircularQueue; import java.util.ArrayList; /** * Created by wx on 2018/1/9. * 使用递归实现邻接链表的BFS算法; * 使用队列实现邻接链表的DFS算法. */ public class XFS<T> { private ArrayList<Vertex<T>> processedVertex; //存储已遍历顶点 private ArrayList<Vertex<T>> graph; //构建好的邻接链表 private Visitor<T> visitor; //访问类 public XFS(myGraph<T> newGraph){ graph = newGraph.getVerList(); processedVertex = new ArrayList<Vertex<T>>(); visitor = new Visitor<T>(); } // 深度优先搜索驱动器 public void DFS(){ clear(); for(Vertex<T> vertex : graph){ if(!processedVertex.contains(vertex)) DFSTraverse(vertex); } } // DFS实际逻辑 private void DFSTraverse(Vertex<T> vertex){ visitor.visit(vertex); //操作节点 markVertex(vertex); //标记节点 for(int i=0; i<vertex.neighbors.size(); i++){ Vertex<T> nerVertex = graph.get(vertex.neighbors.get(i).index); if(!processedVertex.contains(nerVertex)) DFSTraverse(nerVertex); } } //将顶点标记为已处理 private void markVertex(Vertex<T> vertex){ if(!processedVertex.contains(vertex)) processedVertex.add(vertex); } //清除之前成员变量信息 private void clear(){ processedVertex = new ArrayList<Vertex<T>>(); } // 广度优先搜索驱动器 public void BFS(){ clear(); for(Vertex<T> vertex : graph){ if(!processedVertex.contains(vertex)) BFSTraverse(vertex); } } // BFS实际逻辑 private void BFSTraverse(Vertex<T> vertex){ CircularQueue<Vertex<T>> laterVertex = new CircularQueue<Vertex<T>>(graph.size()); laterVertex.enqueue(vertex); while(!laterVertex.isEmpty()){ //遍历访问过的一层顶点 Vertex<T> preVertex = laterVertex.dequeue(); visitor.visit(preVertex); markVertex(preVertex); for(int i=0; i<preVertex.neighbors.size(); i++){ //将顶点的邻居加入待访问队列 Vertex<T> nerVertex = graph.get(vertex.neighbors.get(i).index); if(!processedVertex.contains(nerVertex)) laterVertex.enqueue(nerVertex); } } } }
经测试,运行情况良好!