图的最短路径拓扑排序和关键路径
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
数据结构课程辅导
---图的最短路径、拓扑排序和关键路径
一、最短路径
由图的概念可知,在一个图中,若从一顶点到另一顶点存在着一条路径(这里只讨论无回路的简单路径),则称该路径长度为该路径上所经过的边的数目,它也等于该路径上的顶点数减1。
由于从一顶点到另一顶点可能存在着多条路径,每条路径上所经过的边数可能不同,即路径长度不同,我们把路径长度最短(即经过的边数最少)的那条路径叫做最短路径,其路径长度叫做最短路径长度或最短距离。
上面所述的图的最短路径问题只是对无权图而言的,若图是带权图,则把从一个顶点i到图中其余任一个顶点j的一条路径上所经过边的权值之和定义为该路径的带权路径长度,从v i到v j可能不止一条路径,我们把带权路径长度最短(即其值最小)的那条路径也称作最短路径,其权值也称作最短路径长度或最短距离。
例如,在图3-1中,从v0到v4共有三条路径:{0,4},{0,1,3,4}和{0,1,2,4},其带权路径长度分别为30,23和38,可知最短路径为{0,1,3,4},最短距离为23。
图3-1 带权图和对应的邻接矩阵
实际上,这两类最短路径问题可合并为一类,这只要把无权图上的每条边标上数值为1的权就归属于有权图了,所以在以后的讨论中,若不特别指明,均认为是求带权图的最短路径问题。
求图的最短路径问题用途很广。
例如,若用一个图表示城市之间的运输网,图的顶点代表城市,图上的边表示两端点对应城市之间存在着运输线,边上的权表示该运输线上的运输时间或单位重量的运费,考虑到两城市间的海拔高度不同,流水方向不同等因素,将造成来回运输时间或运费的不同,所以这种图通常是一个有向图。
如何能够使从一城市到另一城市的运输时间最短或者运费最省呢?这就是一个求两城市间的最短路径问题。
求图的最短路径问题包括两个方面:一是求图中一顶点到其余各顶点的最短路径,二是求图中每对顶点之间的最短路径。
下面分别进行讨论。
1. 从一顶点到其余各顶点的最短路径
对于一个具有n个顶点和e条边的图G,从某一顶点v i(称此为源点)到其余任一顶点v j(称此为终点)的最短路径,可能是它们之间的边(i,j)或<i,j>,也可能是经过k个(1≤k≤n-2,最多经过除源点和终点之外的所有顶点)中间顶点和k+1条边所形成的路径。
例如在图3-1中,从v0到v1的最短路径就是它们之间的有向边<0,1>,其长度为3;从v0到v4的最短路径经过两个中间点v1和v3以及三条有向边<0,1>,<1,3>和<3,4>,其长度为23。
那么,如何求出从源点i到图中其余每一个顶点的最短路径呢?狄克斯特拉(Dijkstra)于1959年提出了解决此问题的一般算法,具体做法是按照从源点到其余每一顶点的最短路径长度的升序依次求出从源点到各顶点的最短路径及长度,每次求出从源点i到一个终点m的最短路径及长度后,都要以该顶点m作为新考虑的中间点,用v i到v m的最短路径和最短路径长度对v i到其它尚未求出最短路径的那些终点的当前最短路径及长度作必要地修改,使之成为当前新的最短路径和最短路径长度,当进行n-2次(因最多考虑n-2个中间点)后算法结束。
狄克斯特拉算法需要设置一个集合,假定用S表示,其作用是保存已
求得最短路径的终点序号,它的初值中只有一个元素,即源点i,以后每求出一个从源点i到终点m的最短路径,就将该顶点m并入S集合中,以便作为新考虑的中间点;还需要设置一个整型(假定权值类型为整型)数组dist[MaxVertexNum],该数组中的第j个元素dist[j]用来保存从源点i到终点j的目前最短路径长度,它的初值为(i,j)或<i,j>边上的权值,若v i到v j没有边,则权值为MaxValue,以后每考虑一个新的中间点时,dist[j]的值可能变小;另外,再设置一个与dist数组相对应的、类型为adjlist的邻接表表头向量数组path,该数组中的第j个元素path[j]指向一个单链表,该单链表中保存着从源点i到终点j的目前最短路径,即一个顶点序列,当v i到v j存在着一条边时,则path[j]初始指向由顶点i和j构成的单链表,否则path[j]的初值为空。
此算法的执行过程是:首先从S集合以外的顶点(即待求出最短路径的终点)所对应的dist数组元素中,查找出其值最小的元素,假定为dist[m],该元素值就是从源点i到终点m的最短路径长度(证明从略),对应path数组中的元素path[m]所指向的单链表链接着从源点i到终点m 的最短路径,即经过的顶点序列或称边序列;接着把已求得最短路径的终点m并入集合S中;然后以v m作为新考虑的中间点,对S集合以外的每个顶点j,比较dist[m]+GA[m][j](GA为图G的邻接矩阵)与dist[j]的大小,若前者小于后者,表明加入了新的中间点v m之后,从v i到v j的路径长度比原来变短,应用它替换dist[j]的原值,使dist[j]始终保持到目前为止最短的路径长度,同时把path[m]单链表复制到path[j]上,并在其后插入v j结点,使之构成从源点i到终点j的目前最短路径。
重复n-2次上述运算过程,即可在dist数组中得到从源点i到其余每个顶点的最短路径长度,在path数组中得到相应的最短路径。
为了简便起见,可采用一维数组s[n]来保存已求得最短路径的终点的集合S,具体做法是:若顶点j在集合S中,则令数组元素s[j]的值取1,否则取0。
这样,当判断一个顶点j是否在集合S以外时,只要判断对应的数组元素s[j]是否为0即可。
例如,对于图3-1来说,若求从源点v0到其余各顶点的最短路径,则开始时三个一维数组s,dist和path的值为:
s dist path
下面开始进行第一次运算,求出从源点v 0到第一个终点的最短路径。
首先从s 元素为0的对应dist 元素中,查找出值最小的元素,求得dist[2]的值最小,所以第一个终点为v 1, 最短距离为dist[2]=3,最短路径为path[2]={0,1},接着把s[1]置为1,表示v 1已加入S 集合中,然后以v 1为新考虑的中间点,对s 数组中元素为0的每个顶点j (此时2≤j ≤4)的目前最短路径长度dist[j]和目前最短路径path[j]进行必要地修改,因dist[1]+GA[1][2]=3+25=28,小于dist[2]=∞,所以将28赋给dist[2],将path[1]并上v 2后赋给path[2],同理因dist[1]+GA[1][3]=3+8=11,小于dist[3]=∞,所以将11赋给dist[3],将path[1]并上v 3后赋给path[3],最后再看从v 0到v 4,以
v 1作为新考虑的中间点的情况,由于v 1到v 4没有出边,所以GA[1][4]=∞,故dist[1]+GA[1][4]不小于dist[4],因此dist[4]和path[4]无需修改,应维持原值。
至此,第一次运算结束,三个一维数组的当前状态为:
0 1 2 3 4 s dist path
下面开始进行第二次运算,求出从源点v 0到第二个终点的最短路径。
首先从s 数组中元素为0的对应dist 元素中,查找出值最小的元素,求得dist[3]的值最小,所以第二个终点为v 3, 最短距离为dist[3]=11,最短路径为path[3]={0,1,3},接着把s[3]置为1,然后以v 3作为新考虑的中间点,对s 中元素为0的每个顶点j (此时j=3,5)的dist[j]和path[j]进行必要的修改,因dist[3]+GA[3][2]=11+4=15,小于dist[2]=28,所以将15赋给dist[2],将path[3]并上v 2后赋给path[2],同理,因
dist[3]+GA[3][4]=11+12=23,小于dist[4]=30,所以将23赋给dist[4],将path[3]并上v4后赋给path[4]。
至此,第二次运算结束,三个一维数组的当前状态为:
0 1 2 3 4
s Array dist
path
下面开始进行第三次运算,求出从源点v0到第三个终点的最短路径。
首先从s中元素为0的对应dist元素中,查找出值最小的元素为dist[2],所以求得第三个终点为v2,最短距离为dist[2]=15,最短路径为path[2]={0,1,3,2},接着把s[2]置为1,然后以v2作为新考虑的中间点,对s中元素为0的每个顶点j(此时只有v4一个)的dist[j]和path[j]进行必要的修改,因dist[2]+GA[2][4]=15+10=25,大于dist[4]=23,所以无需修改,原值不变。
至此,第三次运算结束,三个一维数组的当前状
态为:
s
dist
path
由于图中有5个顶点,只需运算3次,即n-2次,虽然此时还有一个
顶点未加入S集合中,但它的最短路径及最短距离已经最后确定,所以整
个运算结束。
最后在dist中得到从源v0到每个顶点的最短路径长度,在
path中得到相应的最短路径。
如果用图形表示上述过程中每次运算的结果,则对应的图形分别如图3-2(b)~(e)所示,其中实线有向边所指向的顶点为集合S中的顶点,虚线有向边所指向的顶点为集合S外的顶点;S集合中的顶点上所标数值为从源点v0到该顶点的最短路径长度,从源点v0到该顶点所经过的有向边为从v0到该顶点的最短路径;S集合外的顶点上所标数值为从源点v0到该顶点的目前最短路径长度,从v0到该顶点所经过的有向边为从v0到该顶点的目前最短路径。
为了便于对照分析,把图3-1(a)重画于图3-2(a)中。
图3-2 利用狄克斯特拉算法求最短路径的图形说明
根据以上分析和举例,不难给出狄克斯特拉算法的描述:
void Dijkstra(adjmatrix GA, int dist[],
adjlist path, int i, int n)
//利用狄克斯特拉算法求图GA中从顶点i到其余每个顶点间的//最短距离和最短路径,它们分别被存于数组dist和path数组中
{
int j,k,w,m;
//定义作为集合使用的动态数组s
int* s=new int[n];
//分别给s,dist和path数组赋初值
for(j=0; j<n; j++) {
if(j==i)
s[j]=1;
else
s[j]=0;
dist[j]=GA[i][j];
if(dist[j]<MaxValue && j!=i) {
edgenode* p1=new edgenode;
edgenode* p2=new edgenode;
p1->adjvex=i;
p2->adjvex=j;
p2->next=NULL;
p1->next=p2;
path[j]=p1;
}
else
path[j]=NULL;
}
//共进行n-2次循环,每次求出从源点i到终点m的最短路径及长度 for(k=1; k<=n-2; k++)
{
//求出第k个终点m
w=MaxValue; m=i;
for(j=0; j<n; j++)
if(s[j]==0 && dist[j]<w) {
w=dist[j];
m=j;
}
//若条件成立,则把顶点m并入集合S中,否则退出循环,因为剩余 //的顶点,其最短路径长度均为MaxValue,无需再计算下去 if(m!=i)
s[m]=1;
else
break;
//对s元素为0的对应dist和path中的元素作必要修改 for(j=0; j<n; j++)
if(s[j]==0 && dist[m]+GA[m][j]<dist[j]) {
dist[j]=dist[m]+GA[m][j];
PATH(path, m, j); //调用此函数,由到顶点m的
//最短路径和顶点j构成到顶点j的目前最短路径 }
}
}
PATH函数的定义如下:
void PATH(adjlist path, int m, int j)
//由到顶点m的最短路径和顶点j构成到顶点j的目前最短路径 {
edgenode *p, *q, *s;
//把顶点j的当前最短路径清除掉
p=path[j];
while(p!=NULL) {
path[j]=p->next;
delete p;
p=path[j];
}
//把到顶点m的最短路径拷贝过来到顶点j的最短路径上 p=path[m];
while(p!=NULL) {
q=new edgenode;
q->adjvex=p->adjvex;
if(path[j]==NULL)
path[j]=q;
else
s->next=q;
s=q;
p=p->next;
}
//把顶点j加入到path[j]单链表的最后,形成新的目前最短路径 q=new edgenode;
q->adjvex=j;
q->next=NULL;
s->next=q;
}
2. 每对顶点之间的最短路径
求图中每对顶点之间的最短路径是指把图中任意两个顶点v i和v j(i ≠j)之间的最短路径都计算出来。
若图中有n个顶点,则共需要计算n(n-1)条最短路径。
解决此问题有两种方法:一是分别以图中的每个顶点为源点共调用n次狄克斯特拉算法,因狄克斯特拉算法的时间复杂性为O(n2),所以此方法的时间复杂性为O(n3);二是采用下面介绍的弗洛伊德(Floyed)算法,此算法的时间复杂性仍为O(n3),但比较简单。
弗洛伊德算法从图的邻接矩阵开始,按照顶点v0,v1,…,v n-1的次序,分别以每个顶点v k(0≤k≤n-1)作为新考虑的中间点,在第k-1次运算得到的A(k-1)(A(-1)为图的邻接矩阵GA)的基础上,求出每对顶点v i到v j的目前最短路径长度A(k)[i][j],计算公式为:
A(k)[i][j]=min(A(k-1)[i][j], A(k-1)[i][k]+A(k-1)[k][j])
(0≤i≤n-1,0≤j≤n-1)
其中min函数表示取其参数表中的较小值,参数表中的前项表示在第k-1次运算后得到的从v i到v j的目前最短路径长度,后项表示考虑以v k 作为新的中间点所得到的从v i到v j的路径长度。
若后项小于前项,则表明以v k作为中间点(不排除已经以v0,v1,…,v k-1中的一部分或全部作为其中间点)使得从v i到v j的路径长度变短,所以应把它的值赋给A(k)[i][j],否则把A(k-1)[i][j]的值赋给A(k)[i][j]。
总之,使A(k)[i][j]保存第k次运算后得到的从v i到v j的目前最短路径长度。
当k从0取到n-1后,矩阵A(n-1)就是最后得到的结果,其中每个元素A(n-1)[i][j]就是从顶点v i到v j 的最短路径长度。
对于上面的计算公式,当i=j时变为:
A(k)[i][i]=min(A(k-1)[i][i], A(k-1)[i][k]+A(k-1)[k][i]) (0≤i≤n-1) 若k=0,则参数表中的前项A(-1)[i][i]=GA[i][i]=0,后项A(-1)[i][0]+A(-1)[0][i]必定大于等于0,所以A(0)中的对角线元素同A(-1)中的对角线元素一样,均为0。
同理,当k=1,2,…,n-1时,A(k)中的对角线元素也均为0。
对于上面的计算公式,当i=k或j=k时分别变为:
A(k)[k][j]=min(A(k-1)[k][j], A(k-1)[k][k]+A(k-1)[k][j]) (0≤j≤n-1) A(k)[i][k]=min(A(k-1)[i][k], A(k-1)[i][k]+A(k-1)[k][k]) (0≤i≤n-1) 每个参数表中的后一项都由它的前一项加上A(k-1)[k][k]所组成,因A(k-1)[k][k]=0,所以A(k)[k][j]和A(k)[i][k]分别取上一次的运算结果A(k-1)[k][j]和A(k-1)[i][k]的值,也就是说,矩阵A(k)中的第k行和第k列上的元素均取上一次运算的结果。
下面以求图3-3(a)中每对顶点之间的最短路径长度为例来说明弗洛伊德算法的运算过程。
图3-3 弗洛伊德算法求最短路径的运算过程。
(1) 令k取0,即以v0作为新考虑的中间点,对图3-3(b)所示A(-1)中的每对顶点之间的路径长度进行必要的修改后得到第0次运算结果A(0),如图3-3(c)所示。
在A(0)中,第0行和第0列用虚线框起来表示i=k和j=k的情况,它们同对角线上的元素一样为A(-1)中的对应值,对于其它六个元素,若v i通过新中间点v0然后到v j的路径长度A(-1)[i][0]+A(-1)[0][j]小于原来的路径长度A(-1)[i][j],则用前者修改之,否则仍保持原值。
因v2到v1的路径长度A(-1)[2][1]=5,通过新中间点v0后变短,即为A(-1)[2][0]+A(-1)[0][1]=3+1=4,所以被修改为4,对应的路径为{2,0,1};同样,v2到v3的路径长度通过新中间点v0后也由8变为7,所以被修改为7,对应的路径为{2,0,3};剩余的四对顶点的路径长度,因加入v0作为新中间点后仍不变短,所以保持原值不变。
(2) 令k=1,即以v1作为新考虑的中间点,对A(0)中每对顶点之间的
路径长度进行必要的修改后得到第1次运算结果A(1),如图3-3(d)所示。
此时第1行和第1列同对角线的元素一样,取上一次的值,对于其它六个元素,若v i通过新中间点v1然后到v j的路径长度A(0)[i][1]+A(0)[1][j]小于原来的路径长度A(0)[i][j],则用前者修改之,否则仍保持原值。
因v0到v2的路径长度A(0)[0][2]=∞,通过新中间点v1后变短,即为A(0)[0][1]+A (0)[1][2]=1+9=10,所以被修改为10,对应的路径为{0,1,2};v
0到v3的路径长度A(0)[0][3]=4,通过新中间点v1后变短,即为A(0)[0][1]+A(0)[1][3]=1+2=3,所以也被修改为3,对应的路径为{0,1,3};v2到v3的路径长度A(0)[2][3]=7,通过新中间点v1后也变短,即为A(0)[2][1]+A(0)[1][3]=4+2=6,所以在第一次被修改的基础上又重新被修改为6,对应的路径为A(0)[2][1]的路径{2,0,1}并上A(0)[1][3]的路径{1,3},即为{2,0,1,3};剩余三对顶点的路径长度,因加入新中间点v1后不变短,所以仍保持原值不变。
(3) 令k=2,即以v2作为新考虑的中间点,对A(1)中每对顶点的路径长度进行必要地修改,得到第2次运算的结果,如图3-3(e)所示。
同上两次的分析过程一样,请读者分析这一次结果。
(4) 令k=3,即以v3作为新考虑的中间点,这也是最后一个要考虑的中间点,在A(2)的基础上进行运算,得到的运算结果A(3)如图3-3(f)所示,也请读者同学们自行分析。
A(3)就是最后得到的整个运算的结果,A(3)中的每个元素A(3)[i][j]的值就是图3-3(a)中顶点v i到v j的最短路径长度。
当然相应的最短路径也可以通过另设一个矩阵记录下来。
通过以上分析可知,在每次运算中,对i=k或j=k或i=j的那些元素无需进行计算,因为它们不会被修改,对于其余元素,只有满足A(k-1)[i][k]+A(k-1)[k][j]<A(k-1)[i][j]的元素才会被修改,即把小于号左边的两个元素之和赋给A(k)[i][j],在这两个元素中,一个是列号等于k,一个是行号等于k,所以它们在进行第k次运算的整个过程中,其值都不会改变,故每一次运算都可以在原数组上“就地”进行,即用新修改的值替换原值即可,不需要使用两个数组交替进行。
设具有n个顶点的一个带权图G的邻接矩阵用GA表示,与GA同类型的,求每对顶点之间最短路径长度的二维数组用A表示,A的初值等于GA。
弗洛伊德算法需要在A上进行n次运算,每次以v k(0≤k≤n-1)作为一个新考虑的中间点,求出每对顶点之间的当前最短路径长度,最后一次运算后,A中的每个元素A[i][j]就是图G中从顶点v i到顶点v j的最短路径长度。
利用C++语言编写出弗洛伊德算法如下,假定在该算法中不需要记录每对顶点之间的最短路径,只需要记录每对顶点之间的最短距离。
void Floyed(adjmatrix GA, adjmatrix A, int n)
//利用弗洛伊德算法求GA表示的图中每对顶点之间的最短长度,//对应保存于二维数组A中
{
int i,j,k;
//给二维数组A赋初值,它等于图的邻接矩阵GA for(i=0; i<n; i++)
for(j=0; j<n; j++)
A[i][j]=GA[i][j];
//依次以每个顶点作为中间点,逐步优化数组A for(k=0; k<n; k++)
for(i=0; i<n; i++)
for(j=0; j<n; j++)
{
if(i==k || j==k || i==j)
continue;
if(A[i][k]+A[k][j]<A[i][j])
A[i][j]=A[i][k]+A[k][j];
}
}
二、拓扑排序
一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。
在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工
程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。
为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。
通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
课程代号课程名称先修课程
C1 高等数学无
C2 程序设计基础无
C3 离散数学 C1,C2
C4 数据结构 C3,C5
C5 算法语言 C2
C6 编译技术 C4,C5
C7 操作系统 C4,C9
C8 普通物理 C1
C9 计算机原理 C8
图3-4 课程表
例如,假定一个计算机专业的学生必须完成图3-4所列出的全部课程。
在这里,课程代表活动,学习一门课程就表示进行一项活动,学习每门课程的先决条件是学完它的全部先修课程。
如学习《数据结构》课程就必须安排在学完它的两门先修课程《离散数学》和《算法语言》之后。
学习《高等数学》课程则可以随时安排,因为它是基础课程,没有先修课。
若用AOV 网来表示这种课程安排的先后关系,则如图3-5所示。
图中的每个顶点代表一门课程,每条有向边代表起点对应的课程是终点对应课程的先修课。
图中的每个顶点代表一从图中可以清楚地看出各课程之间的先修和后续的关系。
如课程C5的先修课为C2,后续课程为C4和C6。
图3-5 AOV网图3-6 三个顶点的回路
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行。
如图3-6是一个具有三个顶点的回路,由<A,B>边可得B活动必须在A活动之后,由<B,C>边可得C活动必须在B活动之后,所以推出C活动必然在A活动之后,但由<C,A>边可得C 活动必须在A活动之前,从而出现矛盾,使每一项活动都无法进行。
这种情况若在程序中出现,则称为死锁或死循环,是应该必须避免的。
在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。
AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
例如,下面的三个序列都是图3-5的拓扑序列,当然还可以写出许多。
(1) C1,C8,C9,C2,C3,C5,C4,C7,C6
(2) C2,C1,C3,C5,C4,C6,C8,C9,C7
(3) C1,C2,C3,C8,C9,C5,C4,C6,C7
由AOV网构造出拓扑序列的实际意义是:如果按照拓扑序列中的顶点次序,在开始每一项活动时,能够保证它的所有前驱活动都已完成,从而使整个工程顺序进行,不会出现冲突的情况。
由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。
(1) 选择一个入度为0的顶点并输出之;
(2) 从网中删除此顶点及所有出边。
循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。
下面以图3-7(a)为例,来说明拓扑排序算法的执行过程。
图3-7 拓扑排序的图形说明
(1) 在(a)图中v0和v1的入度都为0,不妨选择v0并输出之,接着删去顶点v0及出边<0,2>,得到的结果如(b)图所示。
(2) 在(b)图中只有一个入度为0的顶点v1,输出v1,接着删去v1和它的三条出边<1,2>,<1,3>和<1,4>,得到的结果如(c)图所示。
(3) 在(c)图中v2和v4的入度都为0,不妨选择v2并输出之,接着删去v2及两条出边<2,3>和<2,5>,得到的结果如(d)图所示。
(4) 在(d)图上依次输出顶点v3,v4和v5,并在每个顶点输出后删除该顶点及出边,操作都很简单,不再赘述。
为了利用C++语言在计算机上实现拓扑排序算法,AOV网采用邻接表表示较方便。
如对于图3-8(a),对应的邻接表如图3-8所示。
图3-8 图3-7(a)的链接表
(其中值的含义:GL[i]的值是i节点的直接后续节点形成的链表)
在拓扑排序算法中,需要设置一个包含n个元素的一维整型数组,假定用d表示,用它来保存AOV网中每个顶点的入度值。
如对于图3-8(a),得到数组d的初始值为
0 0 2 2 1 3
在进行拓扑排序中,为了把所有入度为0的顶点都保存起来,而且又便于插入、删除以及节省存储,最好的方法是把它们链接成一个栈。
另外,当一个顶点v i的入度为0时,数组d中下标为i的元素d[i]的值为0,正好可利用该元素作为链栈中的一个结点使用,保存下一个入度为0的顶点的序号,这样就可以把所有入度为0的顶点通过数组d中的对应元素静态链接成一个栈。
在这个链栈中,栈顶指针top指向第一个入度为0的顶点所对应的数组d中的元素,该元素的值则指向第二个入度为0的顶点所对应的数组d中的元素,依此类推,最后一个入度为0顶点所对应的数组d 中的元素保存着-1,表示为栈底。
例如,根据图3-8所示的邻接表,建立的入度为0的初始栈的过程为: (1) 开始置链栈为空,即给链栈指针top赋初值为-1:
top=-1;
(2) 将入度为0的元素d[0]进栈,即:
d[0]=top; top=0;
此时top指向d[0]元素,表示顶点v0的入度为0,而d[0]的值为-1,表明为栈底。
(3) 将入度为0的元素d[1]进栈,即:
d[1]=top; top=1;
此时top指向d[1]元素,表示顶点v1的入度为0,而d[1]的值为0,表明下一个入度为0的元素为d[0],即对应下一个入度为0的顶点为v0,d[0]的值为-1,所以此栈当前有两个元素d[1]和d[0]。
(4) 因d[2]至d[5]的值均不为0,即对应的v2到v5的入度均不为0,所以它们均不进栈,至此,初始栈建立完毕,得到的数组d为:
将入度为0的顶点利用上述链栈链接起来后,拓扑算法中循环执行的第(1)步“选择一个入度为0的顶点并输出之”,可通过输出栈顶指针top 所代表的顶点序号来实现;第(2)步“从AOV网中删除刚输出的顶点(假定为v j,其中j等于top的值)及所有出边”,可通过首先作退栈处理,使top指向下一个入度为0的元素,然后遍历v j的邻接点表,分别把所有邻接点的入度减1,若减1后的入度为0则令该元素进栈来实现。
此外,该循环的终止条件“直到不存在入度为0的顶点为止”,可通过判断栈空来实现。
对于图3-7(a),当删除由top值所代表的顶点v1及所有出边后,数组d变为:
当依次删除top所表示的每个顶点及所有出边后,数组d的变化分别如图3-9(a)至(d)所示:
(a) 删除顶点v4及所有出边
(b) 删除顶点v0及所有出边
top
(c) 删除顶点v2及所有出边
3
图3-9 数组d变化示意图
当删除顶点v5及所有出边后,top的值为1,表示栈空,至此算法执行结束,得到的拓扑序列为:1,4,0,2,3,5。
根据以上分析,给出拓扑排序算法的具体描述为:
void Toposort(adjlist GL, int n)
//对用邻接表GL表示的有向图进行拓扑排序
{
int i,j,k,top,m=0; //m用来统计拓扑序列中的顶点数
edgenode *p;
//定义存储图中每个顶点入度的一维整型数组d int* d=new int[n];
//初始化数组d中的每个元素值为0
for(i=0; i<n; i++)
d[i]=0;
//利用数组d中的对应元素统计出每个顶点的入度 for(i=0; i<n; i++) {
p=GL[i];
while(p!=NULL) {
j=p->adjvex;
d[j]++;
p=p->next;
}
}
//初始化用于链接入度为0的元素的栈的栈顶指针top为-1 top=-1;
//建立初始化栈
for(i=0; i<n; i++)
if(d[i]==0) {
d[i]=top;
top=i;
}
//每循环一次删除一个顶点及所有出边
while(top!=-1)
{
j=top; //j的值为一个入度为0的顶点序号
top=d[top]; //删除栈顶元素
cout<<j<<' '; //输出一个顶点
m++; //输出的顶点个数加1。