和树的遍历类似,在此,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(TraversingGraph)。如果只访问图的顶点而不关注边的信息,那么图的遍历十分简单,使用一个foreach语句遍历存放顶点信息的数组即可。但如果为了实现特定算法,就需要根据边的信息按照一定顺序进行遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。
图的遍历要比树的遍历复杂得多,由于图的任一顶点都可能和其余顶点相邻接,故在访问了某顶点之后,可能顺着某条边又访问到了已访问过的顶点,因此,在图的遍历过程中,必须记下每个访问过的顶点,以免同一个顶点被访问多次。为此给顶点附设访问标志visited,其初值为false,一旦某个顶点被访问,则其visited标志置为true。
图的遍历方法有两种:
- 深度优先搜索遍历(Depth-First Search 简称DFS)。
- 广度优先搜索遍历(Breadth_First Search 简称BFS)。
注意:
- 如果给你一个图形,然后让你遍历,即使告诉你第一个起点,也不能得到一个唯一的遍历序列。
- 如果给的是一个图的存储结构,然后告诉你访问的其实顶点,那遍历序列唯一。
- 深度优先遍历借助于栈的数据结构,广度优先遍历借助于队列的数据结构。
一,深度优先搜索遍历:
图的深度优先搜索遍历类似于二叉树的深度优先搜索遍历,并且借助于栈的数据结构。其基本思想如下:假定以图中某个顶点Vi为出发点,首先访问出发点,然后选择一个Vi的未访问过的邻接点Vj,以Vj为新的出发点继续进行深度优先搜索,直至图中所有顶点都被访问过。显然,这是一个递归的搜索过程。
现以图8.15为例说明深度优先搜索过程。假定V1是出发点,首先访问V1。因V1有两个邻接点V2、V3均末被访问过,可以选择V2作为新的出发点,访问V2之后,再找V2的末访问过的邻接点。同V2邻接的有V1、V4和V5,其中V1已被访问过,而V4、V5尚未被访问过,可以选择V4作为新的出发点。重复上述搜索过程,继续依次访问V8、V5 。访问V5之后,由于与V5相邻的顶点均已被访问过,搜索退回到V8,访问V8的另一个邻接点V6。接下来依次访问V3和V7,最后得到的的顶点的访问序列为:V1 → V2 → V4 → V8 → V5 → V6 → V3 → V7。
下面根据上一节创建的邻接表存储结构添加深度优先搜索遍历代码。
【例8-2 DFSTraverse.cs】深度优先搜索遍历
打开【例8-1 AdjacencyList.cs】,在AdjacencyList<T>类中添加以下代码后,将文件另存为DFSTraverse.cs。
public void DFSTraverse(){ //深度优先遍历
InitVisited(); //将visited标志全部置为false
DFS(items[0]); //从第一个顶点开始遍历
}
private void DFS(Vertex<T> v){ //使用递归进行深度优先遍历
v.visited = true; //将访问标志设为true
Console.Write(v.data + " "); //访问
Node node = v.firstEdge;
while (node != null){ //访问此顶点的所有邻接点
//如果邻接点未被访问,则递归访问它的边
if (!node.adjvex.visited){
DFS(node.adjvex); //递归
}
node = node.next; //访问下一个邻接点
}
}
private void InitVisited(){ //初始化visited标志
foreach (Vertex<T> v in items){
v.visited = false; //全部置为false
}
}
【例8-2 Demo8-2.cs】深度优先搜索遍历测试
using System;
class Demo8_2{
static void Main(string[] args){
AdjacencyList<string> a = new AdjacencyList<string>();
a.AddVertex("V1");
a.AddVertex("V2");
a.AddVertex("V3");
a.AddVertex("V4");
a.AddVertex("V5");
a.AddVertex("V6");
a.AddVertex("V7");
a.AddVertex("V8");
a.AddEdge("V1", "V2");
a.AddEdge("V1", "V3");
a.AddEdge("V2", "V4");
a.AddEdge("V2", "V5");
a.AddEdge("V3", "V6");
a.AddEdge("V3", "V7");
a.AddEdge("V4", "V8");
a.AddEdge("V5", "V8");
a.AddEdge("V6", "V8");
a.AddEdge("V7", "V8");
a.DFSTraverse();
}
}
运行结果: V1 V2 V4 V8 V5 V6 V3 V7
二:广度优先搜索遍历
图的广度优先搜索遍历算法是一个分层遍历的过程,和二叉树的广度优先搜索遍历类同,借助于队列的数据结构。。它从图的某一顶点Vi出发,访问此顶点后,依次访问Vi的各个未曾访问过的邻接点,然后分别从这些邻接点出发,直至图中所有已有已被访问的顶点的邻接点都被访问到。对于图8.15所示的无向连通图,若顶点Vi为初始访问的顶点,则广度优先搜索遍历顶点访问顺序是:V1 → V2 → V3 → V4 → V5 → V6 → V7 → V8。遍历过程如图8.16的所示。
和二叉树的广度优先搜索遍历类似,图的广度优先搜索遍历也需要借助队列来完成,例8.3演示了这个过程。
打开【例8-2 DFSTraverse.cs】,在AdjacencyList<T>类中添加以下代码后,将文件另存为BFSTraverse.cs。
public void BFSTraverse(){ //广度优先遍历
InitVisited(); //将visited标志全部置为false
BFS(items[0]); //从第一个顶点开始遍历
}
private void BFS(Vertex<T> v) {//使用队列进行广度优先遍历
//创建一个队列
Queue<Vertex<T>> queue = new Queue<Vertex<T>>();
Console.Write(v.data + " "); //访问
v.visited = true; //设置访问标志
queue.Enqueue(v); //进队
while (queue.Count > 0){ //只要队不为空就循环
Vertex<T> w = queue.Dequeue();
Node node = w.firstEdge;
while (node != null){ //访问此顶点的所有邻接点
//如果邻接点未被访问,则递归访问它的边
if (!node.adjvex.visited){
Console.Write(node.adjvex.data + " "); //访问
node.adjvex.visited = true; //设置访问标志
queue.Enqueue(node.adjvex); //进队
}
node = node.next; //访问下一个邻接点
}
}
}
using System;
class Demo8_3
{
static void Main(string[] args)
{
AdjacencyList<string> a = new AdjacencyList<string>();
a.AddVertex("V1");
a.AddVertex("V2");
a.AddVertex("V3");
a.AddVertex("V4");
a.AddVertex("V5");
a.AddVertex("V6");
a.AddVertex("V7");
a.AddVertex("V8");
a.AddEdge("V1", "V2");
a.AddEdge("V1", "V3");
a.AddEdge("V2", "V4");
a.AddEdge("V2", "V5");
a.AddEdge("V3", "V6");
a.AddEdge("V3", "V7");
a.AddEdge("V4", "V8");
a.AddEdge("V5", "V8");
a.AddEdge("V6", "V8");
a.AddEdge("V7", "V8");
a.BFSTraverse(); //广度优先搜索遍历
}
}
运行结果: V1 V2 V3 V4 V5 V6 V7 V8
非连通图的遍历:
以上讨论的图的两种遍历方法都是相对于无向连通图的,它们都是从一个顶点出发就能访问到图中的所有顶点。若无向图是非连通图,则只能访问到初始点所在连通分量中的所有顶点,其他连通分量中的顶点是不可能访问到的(如图8.17所示)。为此需要从其他每个连通分量中选择初始点,分别进行遍历,才能够访问到图中的所有顶点,否则不能访问到所有顶点。为此同样需要再选初始点,继续进行遍历,直到图中的所有顶点都被访问过为止。
public void DFSTraverse() //深度优先遍历
{
InitVisited(); //将visited标志全部置为false
foreach (Vertex<T> v in items)
{
if (!v.visited) //如果未被访问
{
DFS(v); //深度优先遍历
}
}
}
public void BFSTraverse() //广度优先遍历
{
InitVisited(); //将visited标志全部置为false
foreach (Vertex<T> v in items)
{
if (!v.visited) //如果未被访问
{
BFS(v); //广度优先遍历
}
}
}
深度遍历、广度遍历的时间复杂度:
- 在邻接矩阵上遍历,一般至少需要将矩阵中元素一半给过一下,由于矩阵元素个数为 n2 ,因此时间复杂度就是 O(n2) 。
- 至于在邻接表上遍历时,过程与这个类似,但是邻接表中只是存储了边结点(e条边,无向图也只是2e个结点),加上表头结点为n(也就是顶点个数),因此时间复杂度为O(n+e)。
- 另外,在邻接表中判断某个顶点是否关联,最坏时可能需要将链表中所有结点都遍历完(尤其是有向图中),此时时间复杂度自然就是O(e)了。