拓 扑 排 序
一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(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)的链接表
在拓扑排序算法中,需要设置一个包含n个元素的一维整型数组,假定用d表示,用它来保存AOV网中每个顶点的入度值。如对于图3-8(a),得到数组d的初始值为
0 1 2 3 4 5
0 |
0 |
2 |
2 |
1 |
3 |
在进行拓扑排序中,为了把所有入度为0的顶点都保存起来,而且又便于插入、删除以及节省存储,最好的方法是把它们链接成一个栈。另外,当一个顶点vi的入度为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 2 3 4 5
-1 |
0 |
2 |
2 |
1 |
3 |
top
将入度为0的顶点利用上述链栈链接起来后,拓扑算法中循环执行的第(1)步“选择一个入度为0的顶点并输出之”,可通过输出栈顶指针top所代表的顶点序号来实现;第(2)步“从AOV网中删除刚输出的顶点(假定为vj,其中j等于top的值)及所有出边”,可通过首先作退栈处理,使top指向下一个入度为0的元素,然后遍历vj的邻接点表,分别把所有邻接点的入度减1,若减1后的入度为0则令该元素进栈来实现。此外,该循环的终止条件“直到不存在入度为0的顶点为止”,可通过判断栈空来实现。
对于图3-7(a),当删除由top值所代表的顶点v1及所有出边后,数组d变为:
0 1 2 3 4 5
-1 |
|
1 |
1 |
0 |
3 |
top
当依次删除top所表示的每个顶点及所有出边后,数组d的变化分别如图3-9(a)至(d)所示:
0 1 2 3 4 5
-1 |
|
1 |
1 |
|
2 |
top
(a) 删除顶点v4及所有出边
0 1 2 3 4 5
|
-1 |
1 |
|
2 |
top
(b) 删除顶点v0及所有出边
0 1 2 3 4 5
|
|
|
-1 |
|
1 |
top
(c) 删除顶点v2及所有出边
0 1 2 3 4 5
|
|
|
|
|
-1 |
top
(d) 删除顶点v3及所有出边
图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
p=GL[j]; //p指向vj邻接点表的第一个结点
while(p!=NULL)
{
k=p->adjvex; //vk是vj的一个邻接点
d[k]--; //vk的入度减1
if(d[k]==0) { //把入度为0的元素进栈
d[k]=top;
top=k;
}
p=p->next; //p指向vj邻接点表的下一个结点
}
}
cout<<endl;
if(m<n)
//当输出的顶点数小于图中的顶点数时,输出有回路信息
cout<<"The network has a cycle!"<<endl;
}
拓扑排序实际上是对邻接表表示的图G进行遍历的过程,每次访问一个入度为0的顶点。若图G中没有回路,则需要扫描邻接表中的所有边结点,再加上在算法开始时,为建立入度数组d需要访问表头向量中的每个域和其单链表中的每个结点,所以此算法的时间复杂性为O(n+e)。