我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次。
这一过程就叫做图的遍历。
图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。
然而,图的遍历要比树的遍历复杂得多。
因为图的任一顶点都可能和其余的顶点相邻接。
所以,在访问了某个顶点之后,可能沿着某条路径搜索之后,又回到该顶点上。
为了避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点。
为此,我们可以设一个辅助数组visited[0...n-1],它的初始值置为“假”或者零,一旦访问了顶点vi,便置visted[i]为“真”或者为被访问时的次序号。
通常有两条遍历图的路径:深度优先搜索和广度优先搜索。
它们对于无向图和有向图都适用。
===================================================
深度优先搜索
(Depth First Search) DFS
这种遍历类似于树的先根遍历,是树的先根遍历的一种推广。
图(a) 是一张无向图
图(b)是深度优先搜索的过程
图(c)是广度优先搜索的过程
假设初始状态是图中所有顶点未曾被访问,则深度优先搜索可从图中某个顶点v出发,访问此顶点,然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到;
若此图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起点,重复上述过程,直至图中所有顶点都被访问为止。
为了在遍历过程中便于区分顶点是否已被访问,许附设访问标志数组visited[0 ... n-1],其初值为“false”,一旦某个顶点被访问,则其相应的分量置为“true”。
1 Boolean visited[MAX]; 2 Status (*VisitFunc)(int v); 3 4 void DFSTraverse(Graph G, Status(* Visit)(int v)) { //对图G作深度优先遍历 5 VisitFunc = Visit; //使用全局变量VisitFunc,使DFS不必设函数指针参数 6 for(v=0; v<G.vexnum; ++v) visited[v] = FALSE; //访问标志数组初始化 7 for(v=0; v<G.vexnum; ++v) 8 if(!visited[w]) DFS(G, w); //对尚未访问的顶点调用DFS 9 10 } 11 12 13 void DFS(Graph G, int v) { 14 //从第v个顶点出发递归地深度优先遍历图G 15 visited[v] = TRUE; 16 VisitFunc(v); //访问第v个顶点 17 for(w = FirstAdjVex(G,v); w>=0; w=NextAdjVex(G,v,w)) 18 if(!visited[w]) DFS(G, w); //对尚未访问的邻接顶点w,递归调用DFS 19 20 }
遍历的过程实际上是对每个顶点查找其邻接点的过程。其耗费的时间取决于所采用的存储结构。
当使用二维数组表示邻接矩阵作为图的存储结构时,查找每个顶点的邻接点所需时间为O(n^2),其中n为图中顶点数。
当以邻接表作为图的存储结构时,找邻接点所需时间为O(e),其中e为无向图中的边的书或有向图中弧的数。
因此当以邻接表作为存储结构时,深度优先搜索遍历图的时间复杂度为O(n+e)。
===================================================
广度优先搜索
(Breadth First Search) BFS
广度优先搜索遍历类似于树的按层次遍历的过程。
假设从图中的某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,
然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,
直到图中所有已被访问的顶点的邻接点都被访问到。
若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,
直至图中所有顶点都被访问到为止。
换句话说,广度优先搜索遍历图的过程是以v为起始点,由近至远,依次访问v有路径相通且路径长度为1,2,...的顶点。
例如对图a进行广度优先搜索遍历图如图c所示。
和深度优先搜索类似,在遍历的过程中也需要一个访问标志数组。
并且,为了顺次访问路径长度为2、3、...的顶点,需附设队列以存储已被访问的路径长度为1,2,...的顶点。
1 void BFSTraverse(Graph G, Status(*Visit)(int v)) { 2 //按广度优先非递归遍历图G。使用辅助队列Q和访问标志数组visited 3 for(v = 0; v<G.vexnum; ++v) 4 visited[v] = FALSE; 5 InitQueue(Q); //置空辅助队列Q 6 for(v = 0; v<G.vexnum; ++v) 7 { 8 if(!visited[v]) 9 { 10 visited[v] = TRUE; 11 Visit(v); 12 EnQueu(Q, v) //v入队列 13 while(!QueueEmpyt(Q)) { 14 DeQueue(Q,u); //队头元素出队并置为u 15 for(w= FirstAdjVex(G,u); w>=0; w=NextAdjVex(G,u,w)) 16 if(!Visited[w]) { //w为u的尚未访问的邻接顶点 17 Visited[w] = TRUE; 18 Visite(w); 19 EnQueue(Q,W); 20 } 21 } 22 } 23 24 }
分析上述算法,每个顶点至多进一次队列。
遍历图的过程实质上通过边或弧找邻接点的过程。
因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两者不同之处仅仅在于对顶点访问的顺序不同。