拓 撲 排 序
一個較大的工程往往被划分成許多子工程,我們把這些子工程稱作活動(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)。