本篇来实现图的广度和深度遍历操作。
以前在刚学数据结构的时候,看着老师在讲台上讲图的遍历操作时,思路还能跟上。但是当时想到要把这个玩意儿变成代码,想想都觉得可怕。
但是很多事情不去尝试下就放弃了的话,就弄不清楚到底是问题到底真滴是不是很困难,因为有些问题总是看起来比较麻烦,但其实很简单。
就像中学时代学数学的时候,我们数学老师也是我们班主任对我们说的那样,有些题目会有很多行字来描述这个问题,使得学生第一眼看上去就觉得非常困难,但是等你真的阅读完题目后往往都很简单,那些只用寥寥数语就把题目描述完毕的问题反而会特别的困难。
代码结构
本篇只解决两个问题,图的深度遍历和广度遍历。所以显然只要两个函数就解决了,但是考虑到整张图中并不一定是连通的,这个时候遍历就需要输出图中各个连通分量。
以广度优先遍历为例,BFSTraverse(x)函数每次从顶点x出发,使用_BFS()函数遍历图中以x为起点的一个连通分量。如果从x开始不能遍历到图中的所有顶点,则BFSTraverse(x)会尝试从另一个没有被访问过的顶点开始求其连通分量,直到图中所有的顶点都被访问过了。
其中visted数组的作用就是标记顶点是否有被访问过。
class
广度优先遍历
思路:
首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1,w2,……,然后再依次访问w1,w2,……,wi的所有未被访问过的邻接顶点。
再从这些访问过的顶点出发,再访问他们所有未被访问过的邻接顶点,依次类推,直到所有的顶点都被访问过。
如果无向图是连通的,则从给定的顶点出发,仅需一次遍历就能够访问图中所有的顶点,如果无向图是非连通的,则给定的顶点出发,一次遍历只能访问到该顶点所在的连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。
对于有向图来说,如果从初始点到图中的每个顶点都有路径,则能够访问到图中所有的顶点,否则不能访问到所有的顶点。
使用一个visited数组来标记顶点是否被访问过。
BFSTraverse
// 实际进行广度遍历的函数,每次遍历都是得到一个以顶点x为起点的连通分量
BFSTraverse()函数:
初始时,BFSTraverse()函数设置一个visited数组,将其全部赋值为false表示所有的节点都没有被访问过。然后使用_BFS()函数来求以顶点x为起点的连通分量,并将visited数组作为参数传入。
当_BFS()函数求出以顶点x为起点的连通分量后,BFSTraverse()函数则再次遍历visited数组,查看是否还有未被访问过的节点。如果还有,则再次调用_BFS()函数,并传入未被访问过的顶点和visited数组。等到所有的节点都被访问过后,返回结果。
_BFS()函数:
_BFS()函数中使用了两个循环,外层循环每次从队列中取出一个顶点,使用内层循环依次访问该顶点的边表中的所有节点。
内层循环不断的将当前起始节点的所有边表节点加入队列(只有在这些节点未被访问过时),当遍历完当前顶点的所有的边表节点后,从队列中取出一个节点再次开始循环,直到队列为空结束。
简而言之,_BFS()函数干的事情就是从顶点x出发,依次访问与x相邻的节点,然后再从这些相邻的节点挨个出发再访问各自的相邻节点。为了不重复访问同一个节点,使用visited数组做访问标识,如果发现要访问的节点已经被访问过了就跳过这个节点。
从网上随便找了一张图来测试下所写的广度优先遍历:
首先需要构建出这张图来:
let
输出这张图的邻接列表看看:
for
从这个输出邻接列表来看,图已经被顺利的构造出来了。
下面分别从顶点V0到V8依次广度优先遍历这棵树:
for
可见这是从顶点V0开始的广度优先遍历中正确的一种。
如果断开V0和V1,V5的连接, 且还是从V0开始遍历会发生什么呢?
此时断开后的图如下所示:
首先断开V0和V1、V5的连接:
myGraph
可以看到V0成了一个单独的连通分量。
深度优先遍历
思路:
首先访问图中某一个起始顶点x,然后由x出发,访问与x邻接且未被访问的任一个顶点w1,再访问与w1邻接的未被访问的任一个顶点w2,重复这个过程,当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直到图中所有的顶点都被访问过为止。
与广度优先遍历一样,从某个给定顶点开始,如果无向图是连通的,则这个给定的顶点出发,仅需一次遍历就能够访问图中所有的顶点,如果无向图是非连通的,则从这个顶点出发,一次遍历只能访问到该顶点所在的连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。
对于有向图来说,如果从初始点到图中的每个顶点都有路径,则能够访问到图中所有的顶点,否则不能访问到所有的顶点。
代码如下:
DFSTraverse
// 实际进行深度遍历的函数,每次遍历都是得到一个以顶点x为起点的连通分量
与广度优先遍历一样,深度优先遍历也使用了一个标记数组,作用是相同的。
DFSTraverse()函数:
初始时,DFSTraverse()函数设置一个visited数组,将其全部赋值为false表示所有的节点都没有被访问过。然后使用_DFS()函数来求以顶点x为起点的连通分量,并将visited数组作为参数传入。
当_DFS()函数求出以顶点x为起点的连通分量后,DFSTraverse()函数再次遍历visited数组,查看是否还有未被访问过的节点。如果还有,则再次调用_DFS()函数,并传入未被访问过的顶点和visited数组。等到所有的节点都被访问过后,返回结果。
_DFS()函数:
在思路中说道
当不能再继续向下访问时,依次退回到最近被访问的顶点
这就想到到了使用一个堆栈来辅助完成遍历。每次遇到一个节点,就将其压入堆栈,做已访问标识后访问其一个相邻的节点:
1、如果发现这个节点的相邻节点中有未被访问过的,则访问这个节点,并将其压入堆栈,做已访问标识,再访问这个节点的一个相邻节点...
2、如果这个节点的所有相邻节点都被访问过了,而此时堆栈还不为空,就将其弹出去。再取堆栈的栈顶元素重复步骤1的操作,如果这个节点的所有相邻节点又全被访问过了,就再将其弹出去...,依此往复,直到堆栈为空结束。
同样使用图1分别从顶点V0到V8对其进行深度优先遍历操作:
for
使用图2来测试下:
for