图的遍历
作者:互联网
6.5 图的遍历
和树的遍历类似,图的遍历也是从图中某一顶点出发,按照某种方法对图中所有顶点访问且仅访问一次。 图的遍历算法是求解图的连通性问题、 拓扑排序和关键路径等算法的基础。
然而,图的遍历要比树的遍历复杂得多。 因为图的任一顶点都可能和其余的顶点相邻接。 所 以在访问了某个顶点之后,可能沿着某条路径搜索之后,又回到该顶点上。 例如,图6.1(b)中 所示的 G2, 由于图中存在回路, 因此在访问了 v1 、 v2 、 v3 、 v4 之后,沿着边<v4, v1>又 可访问到 V1。 为了避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点。 为此,设一 个辅助数组visited[n] , 其初始值置为"false"或者0, 一 旦访问了顶点 Vi, 便置visited[i]为"true"
或者 1。
根据搜索路径的方向,通常有两条遍历图的路径:深度优先搜索和广度优先搜索。 它们对无向图和有向图都适用。
6.5.1 深度优先搜索
1.深度优先搜索遍历的过程
(1)从图中某个顶点v出发, 访问v。
(2)找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。 以该顶点为新顶点,重 复此步骤, 直至刚访问过的顶点没有未被访问的邻接点为止。
(3)返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的 邻接点, 访问该顶点。
(4)重复步骤 (2) 和(3), 直至图中所有顶点都被访问过,搜索结束。
2.深度优先搜索遍历的算法实现
算法6.3 深度优先搜索遍历连通图
-
算法描述:
bool visited[MVNum); //访问标志数组,其初值为 "false" void DFS(Graph G,int v) {//从第 v 个顶点出发递归地深度优先遍历图G cout<<v;visited[v)=true; //访问第v个顶点,并置访问标志数组相应分扯值为 for(w=FirstAdjVex(G,v);w>=O;w=NextAdjVex(G,v,w)) //依次检查v的所有邻接点w , FirstAdjVex(G, v)表示v的第一个邻接点 //NextAdjVex(G,v,w)表示v相对千w的下一个邻接点, w匀表示存在邻接点 if(!visited[w]) DFS(G,w); //对v的尚未访问的邻接顶点 w 递归调用 DFS
算法6.4 深度优先搜索遍历非连通图
-
算法描述:
void DFSTraverse(Graph G) {//对非连通图G做深度优先遍历 for(v=O;v<G.vexnum;++v) visited[v]=false; for(v= O;v<G.vexnum;++v) if(!visited[v]) DFS(G,v); }
算法6.5 采用邻接表表示图的深度优先搜索遍历
-
算法描述:
void DFS_AM(AMGraph G,int v) {//图G为邻接矩阵类型,从第v个顶点出发深度优先搜索遍历图G cout<<v;visited[v]=true; //访问第v个顶点,并置访问标志数组相应分址值为true for(w=O;w<G.vexnum;w++) //依次检查邻接矩阵v所在的行 if((G.arcs[v][w]!=O}&&(!visited[w]}} DFS(G,w}; //G.arcs[v][w]!=0表示w是v的邻接点, 如果w未访问, 则递归调用DFS }
算法6.6 采用邻接表表示图的深度优先搜索遍历
-
算法描述:
void DFS_AL (ALGraph G,int v) {//图G为邻接表类型, 从第v个顶点出发深度优先搜索遍历图G cout<<v;visited[v]=true; p=G.vertices[v].firstarc; while(p!=NULL) { w=p->adjvex; if(!visited[w]) DFS(G,w); p=p->nextarc; } }
3.深度优先搜索遍历的算法分析
分析上述算法,在遍历图时,对图中每个顶点至多调用一次 DFS 函数,因为一旦某个顶点被标志成巳被访问,就不再从它出发进行搜索。 因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间则取决千所采用的存储结构。 当用邻接矩阵表示图时,查找每个顶点的邻接点的时间复杂度为 O(n2 ), 其中 n为图中顶点数。而当以邻接表做图的存储结构时,查找邻接点的时间复杂度为O(e), 其中e为图中边数。由此, 当以邻接表做存储结构时,深度优先搜索遍历图的时间复杂度为 O(n + e)。
6.5.2 广度优先搜索
1.广度优先搜索的过程:
(1) 从图中某个顶点v出发, 访问v。
(2) 依次访问v的各个未曾访问过的邻接点。
(3) 分别从这些邻接点出发依次访问它们的邻接点, 并使 “先被访问的顶点的邻接点“ 先千 ”后被访问的顶点的邻接点” 被访问。重复步骤(3), 直至图中所有已被访问的顶点的邻接点都被 访间到。
具体过程如下:
(1) 从顶点 v1 出发,访问 v1
(2) 依次访问 v1 的各个未曾访问过的邻接点 v2 和 v3
(3) 依次访问v2的邻接点v4和v5, 以及v3的邻接点v6和v7, 最后访问v4的邻接点vg。由千 这些顶点的邻接点均已被访问, 并且图中所有顶点都被访问, 由此完成了图的遍历。得到的顶点 访间序列为:
v1-v2-v3-v4-v5-v6-v7-v8
2.广度优先搜索遍历的算法实现
算法6.7 广度优先搜索遍历连通图
-
算法描述:
void BFS{Graph G,int v) {//按广度优先非递归遍历连通图G cout<<v;visited[v]=true; InitQueue(Q); EnQueue(Q,v); while(!QueueEmpty(Q)) { DeQueue(Q,u); for(w=FirstAdjVex(G,u);w>= O;w=NextAdjVex(G,u,w)) if(!visited[w]) { cout<<w; visited[w]=true; EnQueue(Q,v); } } }
3.广度优先搜索遍历的算法分析
分析上述算法,每个顶点至多进一次队列。遍历图的过程实质上是通过边找邻接点的过程,因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,即当用邻接矩阵存储时,时间复杂度为O(n2 ); 用邻接表存储时,时间复杂度为O(n+ e)。两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。
图的应用
6.6.1 最小生成树
假设要在 n 个城市之间建立通信联络网,则连通 n 个城市只需要 n-1 条线路。这时, 自然会考虑这样一个问题, 如何在最节省经费的前提下建立这个通信网。
在每两个城市之间都可设置一条线路,相应地都要付出一定的经济代价。n个城市之间,最多可能设置 n(n- 1)/2 条线路,那么, 如何在这些可能的线路中选择 n-1,以使总的耗费最少呢?
可以用连通网来表示n个城市, 以及n个城市间可能设置的通信线路, 其中网的顶点表示城市,边表示两城市之间的线路,赋予边的权值表示相应的代价。 对于 n 个顶点的连通网可以建立许多不同的生成树,每一棵生成树都可以是一个通信网。 最合理的通信网应该是代价之和最小的生成树。 在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的最小代价生成树 (Minimum Cost Spanning Tree), 简称为最小生成树。
构造最小生成树有多种算法,其中多数算法利用了最小生成树的下列一种简称为MST的性质:假设N= (V, E)是一个连通网,U 是顶点集 V 的一个非空子集。若(u, v)是一条具有最小权值(代价)的边,其中uEU, vEV-U, 则必存在一棵包含边(u, v)的最小生成树。
可以用反证法来证明。假设网N的任何一棵最小生成树都不包含(u, v)。设T是连通网上的一棵最小生成树,当将边(u, v)加入到 T中时,由生成树的定义,T中必存在一条包含(u, v)的回路。另一方面,由于T是生成树,则在T上必存在另一条边(u',v'), 其中u'EU,v'EV-U,且 u 和 u'之间、 v 和 v'之间均有路径相通。 删去边(u', v'), 便可消除上述回路, 同时得到另一棵生 成树兀因为(u,v)的权值不高于(u',v'), 则 T的权值亦不高于 T, T是包含(u,v)的一棵最小生成 树。 由此和假设矛盾。
普里姆 (Prim) 算法和克鲁斯卡尔 (Kruskal) 算法是两个利用 MST 性质构造最小生成树的 算法。 下面先介绍普里姆算法。
算法6.8 普里姆算法
-
算法步骤
- 首先将初始顶点u加入U中,对其余的每一个顶点vj, 将closedge[j]均初始化为到u的边信息。
- 循环 n-I 次, 做如下处理:
• 从各组边 closedge 中选出最小边 closedge[k], 输出此边;
• 将K加入U中;
• 更新剩余的每组最小边信息closedge[j], 对于 V-U 中的边,新增加了一条从K到j的边, 如果新边的权值比 closed ge[j].lowcost 小,则将 closedge[j].lowcost 更新为新边的权值。
-
算法描述
void MiniSpanTree_Prim(AMGraph G,VerTexType u) {//无向网G以邻接矩阵形式存储, 从顶点u出发构造G的最小生成树T, 输出T的各条边 k=LocateVex(G,u); for(j=0;j<G.vexnum;++j) if(j!=k) closedge[j]={u,G.arcs[k][j]}; closedge[k].lowcost=0; for(i=l;i<G.vexnum;++i) { k=Min(closedge); v0=closedge[k].adjevx; v0=G.vexs[k]; cout<<u0<<v0; closedge[k].lowcost=0; for(j=0;j<G.vexnum;++j) if(G.arcs[k][j]<closedge[j].lowcost) } }
算法6.9 克鲁斯卡尔算法
-
算法步骤
- 将数组Edge中的元素按权值从小到大排序。
- 依次查看数组Edge中的边,循环执行以下操作:
- 依次从排好序的数组Edge中选出一条边(U1,U2);
- 在Vexset中分别查找VJ和Vz所在的连通分量VS1和VS2, 进行判断:
- 如果VS1和VS2不等,表明所选的两个顶点分属不同的连通分量,输出此边,并合并 VS1和VS2两个连通分量;
- 如果VS1和VS2相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下 一条权值最小的边。
-
算法描述
void MiniSpanTree_ Kruskal(AMGraph G) {//无向网G以邻接矩阵形式存储,构造G的最小生成树T, 输出T的各条边 Sort(Edge); //将数组 Edge 中的元素按权值从小到大排序 for(i=O;i<G.vexnum;++i) //辅助数组,表示各顶点自成一个连通分址 Vexset[i]=i; for(i=O;i<G.arcnum;++i) //依次查看数组 Edge 中的边 { v1=LocateVex(G,Edge[i].Head); //v1为边的始点 Head 的下标 v2=LocateVex(G,Edge[i].Tail); //v2为边的终点 Ta过的下标 vs1=Vexset[v1]; //获取边 Edge[i]的始点所在的连通分址 vs1 vs2=Vexset[v2]; //获取边 Edge[i]的终点所在的连通分扯 vs2 if(vsl!=vs2) //边的两个顶点分属不同的连通分量 { cout << Edge[i].Head << Edge[i].Tail;//输出此边 for(j=O;j<G.vexnurn;++j) //合并VS1和VS2两个分量, 即两个集合统一编号 if(Vexset[j]==vs2) Vexset[j]=vsl; //集合编号为vs2的都改为vs1 } //if } //for }
标签:遍历,访问,算法,搜索,邻接,顶点 来源: https://www.cnblogs.com/Dear-bella/p/15878590.html