參考書籍:數據結構(C語言版)嚴蔚敏吳偉民編著清華大學出版社
本文中的代碼可從這裏下載:https://github.com/qingyujean/data-structure
1.有向無環圖
有向無環圖(directed acycline graph)簡稱DAG圖,是描述一項工程或系統的進行過程的有效工具。對整個工程和系統,人們關心的是兩個方面的問題:一是工程能否順利進行;二是估算整個工程完成所必須的最短時間。
有向無環圖的應用:1.拓撲排序;2.關鍵路徑
在工程實踐中,一個工程項目往往由若干個子項目組成,這些子項目間往往有多種關係:
1.先後關係,即必須在一子項目完成後,才能開始實施另一個子項目;
2.子項目之間無次序要求,即兩個子項目可以同時進行,互不影響。
2.拓撲排序
我們用一種有向圖來表示上述問題。在這種有向圖中,頂點表示活動,有向邊表示活動的優先關係,這種有向圖叫做頂點表示活動的網絡(Activity On Vertex Network)簡稱爲AOV網。
在AOV網絡中,如果頂點Vi的活動必須在頂點Vj的活動以前進行,則稱Vi爲Vj的前趨頂點,而稱Vj爲Vi的後繼頂點。這種前趨後繼關係有傳遞性。此外,任何活動i不能以它自己作爲自己的前驅或後繼,這叫做反自反性。
從前驅和後繼的傳遞性和反自反性來看,AOV網中不能出現迴路(有向環),迴路表示頂點之間的先後關係進入了死循環。
判斷AOV網是否有有向環的方法是對該AOV網進行拓撲排序,將AOV網中頂點排列成一個線性有序序列,若該線性序列中包含AOV網全部頂點,則AOV網無環,否則,AOV網中存在有向環,該AOV網所代表的工程是不可行的。
何謂“拓撲排序” ?
拓撲序列:
在AOV網中,若不存在迴路,則所有活動可排列成一個線性序列,使得每個活動的所有前驅活動都排在該活動的前面,我們把此序列叫做拓撲序列。
拓撲排序:
由AOV網構造拓撲序列的過程叫拓撲排序。AOV網的拓撲序列不是唯一的,滿足上述定義的任一線性序列都稱爲它的拓撲序列。
如何進行拓撲排序?
解決方法:
1)從有向圖中選取一個沒有前驅的頂點,並輸出之;
2)從有向圖中刪去此頂點以及所有以它爲尾的弧;
3)重複上述兩步,直至圖空,或者圖不空但找不到無前驅的頂點爲止。後一種情況說明有向圖中存在環。
拓撲排序算法的實現
1.爲了實現拓撲排序的算法,對給定的有向圖可採用鄰接表作爲它的存儲結構。
2.將每個鏈表的表頭結點構成一個順序表,各表頭結點的Data域存放相應頂點的入度值。每個頂點入度初值可隨鄰接表動態生成過程中累計得到。
3.在拓撲排序過程中,凡入度爲零的頂點即是沒有前趨的頂點,可將其取出列入有序序列中,同時將該頂點從圖中刪除掉不再考慮。
刪去一個頂點時,所有它的直接後繼頂點入度均減1,表示相應的有向邊也被刪除掉。
4.設置一個堆棧,將已檢驗到的入度爲零的頂點標號進棧,當再出現新的無前趨頂點時,也陸續將其進棧。每次選入度爲零的頂點時,只要取棧頂頂點即可。
用鄰接表存儲AOV網絡,拓撲排序算法描述:
(1) 把鄰接表中所有入度爲零的頂點進棧;
(2) 在棧不空時:
① 退棧並輸出棧頂的頂點 j;
② 在鄰接表的第 j 個單鏈表中,查找頂點爲 j 的所有直接後繼頂點 k,並將 k 的入度減1。若頂點 k 的入度爲零,則頂點 k 進棧;
③ 若棧空時輸出的頂點個數不是 n,則有向圖中有環路,否則拓撲排序完畢。
示例:
3.代碼實現
3.1定義
/*
DAG 有向無環圖的應用--拓撲排序:能否順利完成工程,即檢查是否存在環,
AOV網:頂點表示活動的網
除了拓撲排序檢查環以外,還可以用DFS
當有向圖中無環時,從圖中某點進行深度優先遍歷時,最先退出DFS函數的頂點即出度爲0的頂點,是拓撲序列中的最後一個頂點,由此,按退出DFS函數的先後記錄下來的頂點序列,即爲逆向的拓撲有序序列
*/
//本次示例採用鄰接表作爲有向圖的存儲結構
#include<stdio.h>
#include<stdlib.h>
/*
圖的表示方法
DG(有向圖)或者DN(有向網):鄰接矩陣、鄰接表(逆鄰接表--爲求入度)、十字鏈表
UDG(無向圖)或者UDN(無向網):鄰接矩陣、鄰接表、鄰接多重表
*/
#define MAX_VERTEX_NUM 10//最大頂點數目
#define NULL 0
typedef int VRType;//對於帶權圖或網,則爲相應權值
typedef int VertexType;//頂點類型
//typedef enum GraphKind {DG, DN, UDG, UDN}; //有向圖:0,有向網:1,無向圖:2,無向
typedef struct ArcNode{
int adjvex;//該弧所指向的頂點的在圖中位置
//VRType w;//弧的相應權值
struct ArcNode *nextarc;//指向下一條弧的指針
}ArcNode;//弧結點信息
typedef struct VNode{
VertexType data;//頂點信息
ArcNode *firstarc;//指向第一條依附該頂點的弧的指針
}VNode, AdjVexList[MAX_VERTEX_NUM];//頂點結點信息
typedef struct{
AdjVexList vexs;//頂點向量
int vexnum, arcnum;//圖的當前頂點數和弧數
//GraphKind kind;//圖的種類標誌
}ALGraph;//鄰接表表示的圖
#define OK 1
#define ERROR 0
typedef int status;
static int indegree[MAX_VERTEX_NUM] = {0};//存放各個頂點的入度的數組
3.2鄰接表表示有向無環圖
//若圖G中存在頂點v,則返回v在圖中的位置信息,否則返回其他信息
int locateVex(ALGraph G, VertexType v){
for(int i = 0; i < G.vexnum; i++){
if(G.vexs[i].data == v)
return i;
}
return -1;//圖中沒有該頂點
}
//採用鄰接表表示法構造有向圖G
void createDG(ALGraph &G){
printf("輸入頂點數和弧數如:(5,3):");
scanf("%d,%d", &G.vexnum, &G.arcnum);
//構造頂點向量,並初始化
printf("輸入%d個頂點(以空格隔開如:v1 v2 v3):", G.vexnum);
getchar();//吃掉換行符
for(int m = 0; m < G.vexnum; m++){
scanf("v%d", &G.vexs[m].data);
G.vexs[m].firstarc = NULL;//初始化爲空指針////////////////重要!!!
getchar();//吃掉空格符
}
//構造鄰接表
VertexType v1, v2;//分別是一條弧的弧尾和弧頭(起點和終點)
//VRType w;//對於無權圖或網,用0或1表示相鄰否;對於帶權圖或網,則爲相應權值
printf("\n每行輸入一條弧依附的頂點(先弧尾後弧頭)如:v1v2:\n");
fflush(stdin);//清除殘餘後,後面再讀入時不會出錯
int i = 0, j = 0;
for(int k = 0; k < G.arcnum; k++){
scanf("v%dv%d",&v1, &v2);
fflush(stdin);//清除殘餘後,後面再讀入時不會出錯
i = locateVex(G, v1);//弧起點
j = locateVex(G, v2);//弧終點
//採用“頭插法”在各個頂點的弧鏈頭部插入弧結點
ArcNode *p1 = (ArcNode *)malloc(sizeof(ArcNode));//構造一個弧結點,作爲弧vivj的弧頭(終點)
p1->adjvex = j;
//p1->w = w;
p1->nextarc = G.vexs[i].firstarc;
G.vexs[i].firstarc = p1;
/*因爲是有向圖,所以不必創建2個弧結點
ArcNode *p2 = (ArcNode *)malloc(sizeof(ArcNode));//構造一個弧結點,作爲弧vivj的弧尾(起點)
p2->adjvex = i;
//p2->w = w;
p2->nextarc = G.vexs[j].firstarc;
G.vexs[j].firstarc = p2;
*/
}
}
3.3拓撲排序的實現
void findInDegree(ALGraph G, int indegree[]){
ArcNode *p;
for(int i = 0; i < G.vexnum; i++){
for(p = G.vexs[i].firstarc; p; p = p->nextarc){
indegree[p->adjvex]++;
}
}
}
//如有向圖無迴路,則輸出G的頂點的一個拓撲序列並返回OK,否則返回ERROR
status toplogicalSort(ALGraph G){
//先初始化各個頂點的入度
findInDegree(G, indegree);
int stack[MAX_VERTEX_NUM];//維護一個棧來存放入度爲0的頂點,當棧爲空時,則說明圖中不存在無前驅的頂點了(即沒有入度爲0的頂點了),說明圖中無環
//否則如果此時仍然存在頂點,而且這些頂點有前驅,則說明有環
int top = 0;//棧頂指針
//將入度爲0的頂點入棧
for(int i = 0; i < G.vexnum; i++){
if(!indegree[i]){
stack[top++] = i;
}
}
int count = 0;//對輸出的頂點計數
ArcNode *p;
while(top != 0){//棧不爲空
int topElemVex_i = stack[--top];//棧頂元素出棧,即第一個無前驅的頂點
printf("v%d ", G.vexs[topElemVex_i].data);//輸出當前結點
count++;
//去掉以該結點爲前驅的點與他的弧,以將相關頂點的入度減1的操作來實現
for(p = G.vexs[topElemVex_i].firstarc; p; p = p->nextarc){
indegree[p->adjvex]--;
if(!indegree[p->adjvex]){
stack[top++] = p->adjvex;//入度爲0者入棧
}
}
}
printf("\n");
if(count < G.vexnum)//該有向圖有迴路
return ERROR;
else
return OK;
}
3.4演示
/*測試:
6,8
v1 v2 v3 v4 v5 v6
v1v2
v1v3
v1v4
v3v2
v4v3
v4v5
v6v4
v6v5
*/
void main(){
ALGraph G;
createDG(G);
//printAdjList(G);
printf("該圖的拓撲排序序列爲:");
toplogicalSort(G);
}
分析:
對有 n 個頂點和 e 條弧的有向圖而言,建立求各頂點的入度的時間複雜度爲O(e);建零入度頂點棧的時間複雜度爲O(n);在拓撲排序過程中,若有向圖無環,則每個頂點進一次棧,出一次棧,入度減1的操作在 WHILE語句中總共執行e次,所以,總的時間複雜度爲O(n+e)。