7.3图的遍历
图的遍历就是从图中的某个顶点出发,按某种方法对图中的所有顶点访问且 仅访问一次。
图的遍历比起树的遍历要复杂得多。由于图中顶点关系是任意的,即图中顶点之间是多对多的关系,图可能是非连通图,图中还可能有回路存在,因此在访问了 某个顶点后,可能沿着某条路径搜索后又回到该顶点上。为了保证图中的各顶点 在遍历过程中访问且仅访问一次,需要为每个顶点设一个访问标志,因此要为图设置一个访问标志数组 visited[n],用于标示图中每个顶点是否被访问过,它的初 始值为 0(假),一旦顶点 vi访问过,则置 visited[i]为 1(真),以表示该顶点已访问。
对于图的遍历,通常有两种方法,即深度优先搜索和广度优先搜索。
1、深度优先搜索
深度优先搜索(Depth_First Search,DFS)是指按照深度方向搜索,它类似 于树的先根遍历,是树的先根遍历的推广。
深度优先搜索的基本思想是:
- (1) 从图中某个顶点 v0出发,首先访问 v0。
- (2) 找出刚访问过的顶点的第一个未被访问的邻接点,然后访问该顶点。 以该定点为新顶点,重复此步骤,直到刚访问过的顶点没有未被访问的 邻接点为止。
- (3) 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点 的下一个未被访问的邻接点,访问该顶点。然后执行步骤(2)。
下图给出了一个深度优先搜索的过程图示,其中实箭头代表访问方向,虚箭头代表回溯方向,箭头旁边的数字代表搜索顺序,A 为起始顶点。
首先访问 A,然后按图中序号对应的顺序进行深度优先搜索。图中序号对应 步骤的解释如下:
- (1)顶点 A 的未访邻接点有 B、E、D,首先访问 A 的第一个未访邻接点 B;
- (2)顶点 B 的未访邻接点有 C、E,首先访问 B 的第一个未访邻接点 C;
- (3)顶点 C 的未访邻接点只有 F,访问 F;
- (4)顶点 F 没有未访邻接点,回溯到 C;
- (5)顶点 C 已没有未访邻接点,回溯到 B;
- (6)顶点 B 的未访邻接点只剩下 E,访问 E;
- (7)顶点 E 的未访邻接点只剩下 G,访问 G;
- (8)顶点 G 的未访邻接点有 D、H,首先访问 G 的第一个未访邻接点 D;
- (9)顶点 D 没有未访邻接点,回溯到 G;
- (10)顶点 G 的未访邻接点只剩下 H,访问 H;
- (11)顶点 H 的未访邻接点只有 I,访问 I;
- (12)顶点 I 没有未访邻接点,回溯到 H;
- (13)顶点 H 已没有未访邻接点,回溯到 G;
- (14)顶点 G 已没有未访邻接点,回溯到 E;
- (15)顶点 E 已没有未访邻接点,回溯到 B;
- (16)顶点 B 已没有未访邻接点,回溯到 A。
至此,深度优先搜索过程结束,相应的访问序列为:A、B、C、F、E、G、 D、H、I。
【算法思想】
首先实现对 v0所在连通子图的深度优先搜索,用递归算法实现的基本过程为:
(1) 访问出发点 v0 。
(2) 依次以 v0的未被访问的邻接点为出发点,深度优先搜索图,直至图中 所有与 v0 有路径相通的顶点都被访问。 若是非连通图,则图中一定还有顶点未被访问,需要从图中另选一个未被访 问的顶点作为起始点,重复上述深度优先搜索过程,直至图中所有顶点均被访问 过为止。
【算法描述】
#define True 1
#define False 0
#define Error –1 /*出错*/
#define Ok 1 int visited[MAX_VERTEX_NUM]; /*访问标志数组*/
void TraverseGraph (Graph g) /* 在图 g 中寻找未被访问的顶点作为起始点,并调用深度优先搜索过程进行遍 历。Graph 表示图的一种存储结构,如邻接矩阵或邻接表等 */
{
for (vi=0; vi<g.vexnum; vi++)
visited[vi]=False ; /*访问标志数组初始*/
for( vi=0; vi<g.vexnum; vi++) /* 循环调用深度优先遍历连通子图的操作 */ /* 若图 g 是连通图,则此调用只执行一次 */
if (!visited[vi] )
DepthFirstSearch(g,vi);
}/* TraverseGraph */
【深度优先遍历图 g 算法】
void DepthFirstSearch(Graph g, int v0) /* 深度遍历 v0 所在的连通子图 */
{
visit(v0);
visited[v0] =True; /*访问顶点 v0,并置访问标志数组相应分量 值*/
w=FirstAdjVertex(g,v0);
while ( w!=-1) /*邻接点存在.*/
{
if(! visited [w] )
DepthFirstSearch(g,w); /* 递 归 调 用 DepthFirstSearch*/
w=NextAdjVertex(g,v0,w); /*找下一个邻接点*/
}
}/*DepthFirstSearch*/
【深度优先遍历 v0 所在的连通子图算法】
上述算法中对于 FirstAdjVertex(g,v0)以及 NextAdjVertex(g,v0,w)并 没有具体实现。如果图的存储结构不同,对应操作的实现方法不同,时间耗费也不同。下面分别用邻接矩阵和邻接表具体实现。
(1)用邻接矩阵方式实现深度优先搜索
【算法描述】
void DepthFirstSearch(AdjMatrix g, int v0) /* 图 g 为邻接矩阵类型 AdjMatrix */
{
visit(v0);
visited[v0]=True;
for ( vj=0;vj<n;vj++)
if (!visited[vj] && g.arcs[v0][vj].adj==1)
DepthFirstSearch(g, vj);
}/* DepthFirstSearch */
【采用邻接矩阵的 DepthFirstSearch 算法】
(2)用邻接表方式实现深度优先搜索
【算法描述】
void DepthFirstSearch(AdjList g, int v0) /*图 g 为邻接表类型 AdjList */
{
visit(v0);
visited[v0]=True;
p=g.vertex[v0].firstarc;
while( p!=NULL )
{
if (! visited[p->adjvex])
DepthFirstSearch(g, p->adjvex);
p=p->nextarc;
}
}/*DepthFirstSearch*/
【采用邻接表的 DepthFirstSearch 算法】
以邻接表作为存储结构,查找每个顶点的邻接点的时间复杂度为 O(e), 其 中 e 是无向图中的边数或有向图中弧数, 则深度优先搜索图的时间复杂度为 O(n+e)。
(3)用非递归过程实现深度优先搜索
【算法思想】
(1) 首先将 v0 入栈;
(2) 只要栈不空,则重复下述处理:
- a) 栈顶顶点出栈,如果未访问,则访问并置访问标志;
- b) 然后将 v0 所有未访问的邻接点入栈。
【算法描述】 非递归形式的 DepthFirstSearch
void DepthFirstSearch(Graph g, int v0) /* 从 v0 出发深度优先搜索图 g */
{
InitStack(S); /*初始化空栈*/
Push(S, v0);
while ( ! Empty(S))
{
v=Pop(S);
if (!visited[v]) /*栈中可能有重复顶点*/
{
visit(v);
visited[v]=True;
}
w= FirstAdjVertex (g, v); /*求 v 的第一个邻接点*/
while (w!=-1 )
{
if (!visited[w])
Push(S, w);
w=NextAdjVertex (g, v, w); /*求 v 相对于 w 的下一个邻接点*/
}
}
}
2、广度优先搜索
广度优先搜索(Breadth_First Search)是指按照广度方向搜索,它类似于树 的层次遍历,是树的按层次遍历的推广。
广度优先搜索的基本思想是:
- (1)从图中某个顶点 v0出发,首先访问 v0。
- (2)依次访问 v0的各个未被访问的邻接点。
- (3)分别从这些邻接点(端结点)出发,依次访问它们的各个未被访问的 邻接点(新的端结点)。访问时应保证:如果 Vi和 Vk为当前端结点,且 Vi在 Vk之前被访问,则 Vi的所有未被访问的邻接点应在 Vk的所有未被 访问的邻接点之前访问。重复(3),直到所有端结点均没有未被访问 的邻接点为止。
若此时还有顶点未被访问,则选一个未被访问的顶点作为起始点,重复上述过程,直至所有顶点均被访问过为止。
下图给出了一个广度优先搜索过程图示,其中箭头代表搜索方向,箭头旁边 的数字代表搜索顺序,A 为起始顶点。
首先访问 A,然后按图中序号对应的顺序进行广度优先搜索。图中序号对应 步骤的解释如下:
- (1)顶点 A 的未访邻接点有 B、E、D,首先访问 A 的第一个未访邻接点 B;
- (2)访问 A 的第二个未访邻接点 E;
- (3)访问 A 的第三个未访邻接点 D;
- (4)由于 B 在 E、D 之前被访问,故接下来应访问 B 的未访邻接点。B 的未访邻接点只有 C,所以访问 C;
- (5)由于 E 在 D、C 之前被访问,故接下来应访问 E 的未访邻接点。E 的未访邻接点只有 G,所以访问 G;
- (6)由于 D 在 C、G 之前被访问,故接下来应访问 D 的未访邻接点。D 没有未访邻接点,所以直接考虑在 D 之后被访问的顶点 C,即接下 来应访问 C 的未访邻接点。C 的未访邻接点只有 F,所以访问 F;
- (7)由于 G 在 F 之前被访问,故接下来应访问 G 的未访邻接点。G 的未 访邻接点只有 H,所以访问 H;
- (8)由于 F 在 H 之前被访问,故接下来应访问 F 的未访邻接点。F 没有 未访邻接点,所以直接考虑在 F 之后被访问的顶点 H,即接下来应 访问 H 的未访邻接点。H 的未访邻接点只有 I,所以访问 I。
至此,广度优先搜索过程结束,相应的访问序列为:A、B、E、D、C、G、 F、H、I。
在遍历过程中需要设立一个访问标志数组 visited[n],其初值为“False”,一 旦某个顶点被访问,则置相应的分量为“True”。同时,需要辅助队列 Q,以便 实现要求:“如果 Vi和 Vk为当前端结点,且 Vi在 Vk之前被访问,则 Vi的所有 未被访问的邻接点应在 Vk的所有未被访问的邻接点之前访问。”
广度优先搜索连通子图的算法如下:
【算法思想】
- (1) 首先访问 v0 并置访问标志,然后将 v0 入队;
- (2) 只要队不空,则重复下述处理: ①队头结点 v 出队; ②对 v 的所有邻接点 w,如果 w 未访问,则访问 w 并置访问标志,然后将 w 入队。
【算法描述】 广度优先搜索图 g 中 v0 所在的连通子图
void BreadthFirstSearch(Graph g, int v0) /*广度优先搜索图 g 中 v0 所在的连通子图*/
{
visit(v0);
visited[v0]=True;
InitQueue(&Q); /*初始化空队*/
EnterQueue(&Q, v0); /* v0 进队*/
while ( ! Empty(Q))
{
DeleteQueue(&Q, &v); /*队头元素出队*/
w=FirstAdjVertex (g, v); /*求 v 的第一个邻接点*/
while (w!=-1)
{
if (!visited[w])
{
visit(w);
visited[w]=True;
EnterQueue(&Q, w);
}
w=NextAdjVertex (g, v, w); /*求 v 相对于 w 的下一个邻接点*/
}
}
}
分析上述算法,图中每个顶点至多入队一次,因此外循环次数为 n。当图 g 采用邻接表方式存储,则当结点 v 出队后,内循环次数等于结点 v 的度。对访问 所有顶点的邻接点的总的时间复杂度为 O(d0+d1+d2+…+dn-1)=O(e),因此图采用 邻接表方式存储,广度优先搜索算法的时间复杂度为 O(n+e);当图 g 采用邻接 矩阵方式存储,由于找每个顶点的邻接点时,内循环次数等于 n,因此广度优先 搜索算法的时间复杂度为 O(n²)。