第6章 图
【学习重点】
① 图的基本术语;
② 图的邻接矩阵存储和邻接表存储;
③ 图的遍历操作及算法实现;
④ 最小生成树算法、最短路径算法、拓扑排序算法和关键路径算法基于的存储结构以及算法的执行过程。
【学习难点】
① 运用图的遍历算法解决图的其他相关问题;
② 最小生成树算法;
③ 最短路径算法;
④ 拓扑排序算法;
⑤ 关键路径算法。
6.1 图的逻辑结构
6.1.1 图的定义和基本术语
在图中常常将数据元素称为顶点。
1.图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G=(V,E) 其中,G表示一个图,V是图G中顶点的集合,E是图G中顶点之间边的集合。若顶点v i 和 v j 之间的边没有方向,则称这条边为无向边,用无序偶对(v i ,v j)来表示;若从顶点v i 到v j 的边有方向,则称这条边为有向边(也称为弧),用有序偶对< v i ,v j >来表示,v i 称为弧尾,v j 称为弧头。如果图的任意两个顶点之间的边都是无向边,则称该图为无向图,否则称该图为有向图。
2.图的基本术语
简单图
在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
邻接、依附
在无向图中,对于任意两个顶点v i 和 v j ,若存在边(v i ,v j ),则称顶点v i 和 v j 互为邻接点,同时称边 (v i ,v j )依附于顶点v i 和 v j 。
在有向图中,对于任意两个顶点v i 和 v j ,若存在弧
无向完全图、有向完全图
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)条边。
在有向图中,如果任意两顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)。
显然,在完全图中,边(或弧)的数目达到最多。
稠密图、稀疏图
称边数很少的图为稀疏图,反之,称为稠密图。稀疏和稠密本身就是模糊的概念,稀疏图和稠密图常常是相对而言的。
顶点的度、入度、出度
在无向图中,顶点v的度是指依附于该顶点的边的个数,记为TD(v)。在具有n个顶点e条边的无向图中,一共有2e条边。
在有向图中,顶点v的入度是指以该顶点为弧头的弧的个数,记为ID(v);顶点v的出度是指以该顶点为弧尾的弧的个数,记为OD(v)。在具有n个顶点e条边的有向图中,一共有e条边。
权、网
在图中,权通常是指对边赋予的有意义的数量值。在实际应用中,权可以有具体的含义。比如,对于城市交通线路图,边上的权表示该条线路的长度或者等级;对于电子线路图,边上的权表示两个端点之间的电阻、电流或电压值;对于工程进度图,边上的权表示从前一个活动到后一个活动所需的时间等。
边上带权的图称为网或网图。
路径、路径长度、回路
在无向图G=(V,E)中,顶点v p 到v q 之间的路径是一个顶点序列v p =v q ,v i1 ,…,v im = v q,其中,(v ij-1,v ij )属于E(1<=j<=m);如果G是有向图,则
显然,在图中路径可能不唯一,回路也可能不唯一。
简单路径、简单回路
在路径序列中,顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路称为简单回路。
子图
对于图G=(V,E)和g=(v,e),如果v在V中且e在E中,则称图g是G的子图。一个图可以有多个子图。
连通图、连通分量
在无向图中,若任意顶点v i和v j(i!=j)之间有路径,则称该图是连通图。非连通图的极大连通图子图称为连通分量,“极大”的含义是指包括所有连通的顶点以及和这些顶点相关联的所有边。
强连通图、强连通分量
在有向图中,对任意顶点v i和v j,若从顶点v i到v j 均有路径,则称该有向图是强连通图。非强连通图的极大强连通图子图称为强连通分量。
生成树、生成森林
具有n个顶点的连通图G的生成树是包含G中全部顶点的一个极小连通子图。连通图的生成树是一个自由树,可以在生成树中任意指定一个顶点为数的根结点。在生成树中添加任意一条属于原图中的边必定会产生回路,因为新添加的边使其所依附的两个顶点之间有了第二条路径;在生成树中减少任意一条边,则必然成为非连通。所以一棵具有n个顶点的生成树有且仅有n-1条边。
具有n个顶点的有向图G的生成树是包含G中全部顶点的一个子图,且子图中只有一个入度为零的顶点,其他顶点的入度均为1。
在非连通图中,由每个连通分量都可以得到一棵生成树,这些连通分量的生成树构成了非连通图的生成森林。
6.1.2图的抽象数据类型定义
ADT Graph
Data
定点的有穷非空集合和边的集合
前置条件:图不存在
输入:n个顶点e条边
功能:图的初始化
输出:无
后置条件:构造一个含有n个顶点e条边的图
DestroyGraph
前置条件:图已存在
输入:无
功能:销毁图
输出:无
后置条件:释放图所占用的存储空间
DFSTraverse
前置条件:图已存在
输入:遍历的起始顶点v
功能:从顶点v出发深度优先遍历图
输出:图的深度优先遍历序列
后置条件:图保持不变
BFSTraverse
前置条件:图已存在
输入:遍历的起始顶点v
功能:从顶点v出发的广度优先遍历图
输出:图的广度优先遍历序列
后置条件:图保持不变
endADT
6.1.3图的遍历操作
图的遍历是指从图中某一顶点出发,对图中所有顶点访问一次且仅访问一次。
(1) 图中没有确定的开始顶点,所以可从图中任意顶点出发,不妨将顶点进行编号,先从编号小的顶点开始。
(2) 要遍历图中所有顶点,只只许多次重复从某一顶点出发进行图的遍历。
(3) 为了在便利过程中便于区分顶点是否已被访问,设置一个访问标志数组visited[n],其初值为未被访问标志“0”。
(4) 图的遍历通常有深度优先遍历和广度优先遍历两种,这两种遍历次序对无向图和有向图都适用。
1. 深度优先遍历
深度优先遍历类似于树的前序遍历
从图中某顶点v出发进行深度优先遍历的基本思想是:
(1) 访问顶点v;
(2) 从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3) 重复上述两步,直至图中所有和v有路径相同的顶点都被访问到。
伪代码:
1. 访问顶点v;visited[v]=1;
2. w=顶点v的第一个邻接点;
3. while(w存在)
3.1 if(w未被访问)从顶点w出发递归执行该算法;
3.2 w=顶点v的下一个邻接点;
2. 广度优先遍历
广度优先遍历类似于树的层序遍历。
从图中某顶点v出发进行广度优先遍历的基本思想是:
(1) 访问顶点v;
(2) 一次访问v的各个未被访问的邻接点v1,,v2.....vk;
(3) 分别从v1,v2,.….vk出发依次访问它们未被访问的邻接点,兵士“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问。
伪代码:
1. 初始化队列Q;
2. 访问顶点v;visited[v]=1;顶点v入队列Q;
3. while(队列Q非空)
3.1 v=队列 Q的队头元素出队;
3.2 w=顶点v的第一个邻接点;
3.3 while(w存在)
3.3.1 如果w未被访问,则访问顶点w;visited[w]=1;顶点w入队列Q;
W=顶点v的下一个邻接点;
6.1.3 图的遍历操作
图的遍历是指从图中某一顶点出发,对图中所有定点访问一次且仅访问一次。
在图的遍历中要解决的关键问题是:
(1)没有一个确定的开始顶点,如何选取遍历的起始顶点?答:一般选编号比较小的顶点。
(2)从某个顶点出发可能到达不了所有其他顶点。答:多次重复从某个顶点出发进行图的遍历。
(3) 如何避免遍历不会因回路而陷入死循环?答:设置一个访问标志数组
(4) 一个定点于多个顶点相邻接,当这样的顶点访问过后,如何选取下一个要访问的顶点?答:利用遍历次序
深度优先遍历:
(1) 访问顶点v;
(2) 从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3) 重复上述两步,直至图中所有和v有路径相通的顶点都被访问。
伪代码:1.访问顶点v visited[v]=1;
2.w=顶点v的第一个邻接点;
3.while (w存在)
3.1 if(w未被访问)从顶点w出发递归执行该算法;
3.2 w=顶点v的下一个邻接点;
广度优先遍历
(1) 访问顶点v;
(2) 依次访问v的各个未被访问的邻接点v1v2…vk;
(3) 分别从v1v2…vk出发访问他们未被访问的邻接点,并使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被被访问。直至图中所有与顶点v有路径相通的顶点都被访问到。
伪代码:
1. 初始化队列Q;
2. 访问顶点v;visited[v]=1;顶点v 入队列Q;
3. while( 队列Q非空)
3.1 v=队列Q的对头元素出队;
3.2 w=顶点v的第一个邻接点;
3.3 while(w存在)
3.3.1 如果w未被访问,则访问顶点w; visited[w]=1;顶点w入队列Q;
3.3.2 w=顶点v的下一个邻接点。
6.2图的存储结构及实现
6.2.1邻接矩阵
图的邻接矩阵存储也称数组表示法,其方法是用用一个以为数组存储图中顶点的信息,用一个二维数组存储图中边的信息,存储顶点之间的邻接关系的二维数组称为邻接矩阵。
无向图的邻接矩阵一定是对称矩阵,而有向图的邻接矩阵则不一定对称。
在图的邻接矩阵存储中容易解决下列问题:
(1) 对于无向图,定点i的度等于邻接矩阵的第i行非零元素的个数。
对于有向图,顶点i的初度等于邻接矩阵低i行非零元素的个数;顶点i的入度等于邻接矩阵第i行非零元素的个数。
(2) 要判断顶点i和j之间是否存在边,只需测试临街矩阵中相应位置的元素arc[i][j],若其值为1,则有边;否则,顶点i和j之间不存在边。
(3) 找顶点i的所有临界点,可依次判别顶点 i与其他顶点之间是否有边或顶点i到其它顶点是否有弧。
邻邻接矩阵存储结构的抽象数据类型定义:
const int MaxSize=10;
template
class MGraph
{
Public :
MGraph(DataType a[],int n,int e);
~MGraph(){}
void DFSTraverse(int v);
void BFSTraverse(int v);
private:
DataType vertex[MaxSize];
int arc[MaxSize][MaxSize];
int vertexNUM,arcNUM;
} ;
邻接矩阵构造函数算法 MGraph
template
MGraph
{
vertexNUM=n;arcNUM=e;
for(i=0;i vertex[i]=a[i]; for(i=0;i for(j=0;j arc[i][j]=0; for(k=0;k { cin>>i>>j; arc[i][j]=1; arc[j][i]=1; } } 深度优先遍历DFSTraverse template void MGraph { cout< for(j=0;j if(arc[v][j]==1&&visited[j]==0)DFSTraverse(j); } 广度优先遍历算法BFSTraverse template void MGraph { front=rear=-1; cout< while(front!=rear) { v=Q[++front]; for(j=0;j ife(arc[v][j]==1&&visited[j]==0) { cout< } } } 6.3 最小生成树 设G=(V,E)是一个无向连通图,生成数上各边的权值之和称为该生成树的代价,在G的所有生成树中,代价最小的生成树称为最小生成树。 6.3.1 MST性质 最小生成树具有MST性质:假设G=(V,E)是一个无向连通网U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必存在一颗包含边(u,v)的最小生成树。 6.3.2 Prim算法 最小生成树算法 Prim void Prim(MGrap G) { for(i=1;i { shortEdge[i].lowcost=G.arc[0][i]; shortEdge[i].adjvex=0; } shortEdge[0].lowcost=0; for (i=1;i { k=MinEdge(shortEdge,G.vertexNum) cout<<"("< shortEdge[k].lowcost=0; for(j=1;j if G.arc[k][j] shortEdge[j].lowcost=G.arc[k][j]; shortEdge[j].adjvex=k; } } } 6.3.3 Kruskal算法 最小生成树算法 Kruskal void Kruskal(EdgeGraph G) { for(i=0;i parent[i]=-1; for(num=0,i=0;i { vex1=FindRoot(parent,G.edge[i].from); vex2=FindRoot(parent,G.edge[i].to); if(vex1!=vex2) { cout<<"("< parent[vex2]=vex1; num++; if (num==n-1) return; } } } int FindRoot(int parent[],int v) { t=v; if(parent[t]>-1) t=parent[t]; return t; } 6.4 最短路径 在非网图中,最短路径是指两顶点之间经历的边数最少的路径,路径上的第一个顶点称为源点,最后一个顶点成为终点。 Dijkstra 算法 void Dijkstra(MGraph G,int v) { for(i=0;i { dist[i]=G.arc[v][i]; if(dist[i]!=∞)path[i]=G.vertex[v]+G.vertex[i]; else path[i]=" "; } s[0]=v; dist[v]=0; num=1; while (num { for(k=0,i=0;i if((dist[i]!=0)&&(dist[i] cout< s[num++]=k; for(i=0;i if(dist[i]>dist[k]+G.arc[k][i]){ dist[i]=dist[k]+G.arc[k][i]; path[i]=path[k]+G.vertex[i]; } dist[k]=0; } } Floyd 算法 void Floyd(MGraph G) { for(i=0;i for(j=0;j { dist[i][j]=G.arc[i][j]; if(dist[i][j]!=∞)path[i][j]=G.vertex[j]; else path[i][j]=" "; } for(k=0;d for(i<0;i for(j=0;j if(dist[i][k]+dist[k][j] dist[i][j]=dist[i][k]+dist[k][j]; path[i][j]=path[i][k]+path[k][j]; } 6.5 有向无环图及其应用 6.5.1 AOV网与拓扑排序 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,称这样的有向图为顶点表示活动的网,简称AOV网 对一个有向图构造拓扑序列的过程称为拓扑排序。 对AOV网进行拓扑排序的基本思想是: (1) 从AOV网中选择一个没有前驱的顶点并且输出它; (2) 从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧; (3) 重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。 显然,拓扑排序的结果有两种:AOV网中全部顶点都被输出,这说明AOV网中不存在回路;AOV网中顶点未被全部输出,剩余的顶点均不存在没有前驱的顶点,这说明AOV网中存在回路。