一:圖
圖狀結構簡稱圖,是另一種非線性結構,它比樹形結構更復雜。樹形結構中的結點是一對多的關系,結點間具有明顯的層次和分支關系。每一層的結點可以和下一層的多個結點相關,但只能和上一層的一個結點相關。而圖中的頂點(把圖中的數據元素稱為頂點)是多對多的關系,即頂點間的關系是任意的,圖中任意兩個頂點之間都可能相關。也就是說,圖的頂點之間無明顯的層次關系,這種關系在現實世界中大量存在。因此,圖的應用相當廣泛,在自然科學、社會科學和人文科學等許多領域都有着非常廣泛的應用。
1.1:圖的基本概念
1.1.1:圖的定義
圖(Graph)是由非空的頂點(Vertex)集合和描述頂點之間的關系——邊(Edge)或弧(Arc)的集合組成。其形式化定義為:
G=(V,E)
V={v i |v i ∈某個數據元素集合}
E={(v i ,v j )|v i ,v j ∈V∧P(v i ,v j )}或E={<v i ,v j >|v i ,v j ∈V∧P(v i ,v j )}
其中,G 表示圖,V 是頂點的集合,E 是邊或弧的集合。在集合 E 中,P(vi,vj)表示頂點 vi 和頂點 vj 之間有邊或弧相連。下圖給出了圖的示例。
在圖 (a)中,V={v 1 ,v 2 ,v 3 ,v 4 ,v 5 }E={(v 1 ,v 2 ),(v 1 ,v 3 ),(v 2 ,v 4 ),(v 2 ,v 5 ),(v 3 ,v 4 ),(v 4 ,v 5 )}
在圖 (b)中,V={v 1 ,v 2 ,v 3 ,v 4 ,v 5 }E={<v 1 ,v 2 >,<v 1 ,v 3 >,<v 2 ,v 3 >,<v 2 ,v 5 ><v 3 ,v 4 >,<v 4 ,v 1 >,<v 4 ,v 5 >}。
注:無向邊用小括號“()”表示,而有向邊用尖括號“<>”表示。
1.1.2:圖的基本術語
1、無向圖:在一個圖中,如果任意兩個頂點v i 和v j 構成的偶對(v i ,v j )∈E是無序的,即頂點之間的連線沒有方向,則該圖稱為無向圖(Undirected Graph)。圖 (a)是一個無向圖。
2、有向圖:在一個圖中,如果任意兩個頂點v i 和v j 構成的偶對<v i ,v j >∈E是有序的,即頂點之間的連線有方向,則該圖稱為有向圖(Directed Graph)。圖(b)是一個有向圖。
3、邊、弧、弧頭、弧尾:無向圖中兩個頂點之間的連線稱為邊(Edge),邊用頂點的無序偶對(v i ,v j )表示,稱頂點v i 和頂點v j 互為鄰接點(Adjacency Point),(v i ,v j )邊依附與頂點v i 和頂點v j 。有向圖中兩個頂點之間的連線稱為弧(Arc),弧用頂點的有序偶對<v i ,v j >表示,有序偶對的第一個結點v i 稱為始點(或弧尾),在圖中不帶箭頭的一端;有序偶對的第二個結點v j 稱為終點(或弧頭),在圖中帶箭頭的一端。
4、無向完全圖:在一個無向圖中,如果任意兩個頂點之間都有邊相連,則稱該圖為無向完全圖(Undirected Complete Graph)。可以證明,在一個含有 n 個頂點的無向完全圖中,有 n(n-1)/2 條邊。
5、有向完全圖:在一個有向圖中,如果任意兩個頂點之間都存在方向互為相反的兩條弧,則稱該圖為有向完全圖(Directed Complete Graph)。可以證明,在一個含有 n 個頂點的有向完全圖中,有 n(n-1)條弧。
6、頂點的度、入度、出度:在無向圖中,頂點 v 的度(Degree)是指依附於頂點 v 的邊數,通常記為 TD(v)。在有向圖中,頂點的度等於頂點的入度(In Degree)與頂點的出度之和。頂點v的入度是指以該頂點v為弧頭的弧的數目,記為ID(v);頂點 v 的出度(Out Degree)是指以該頂點 v 為弧尾的弧的數目,記為 OD(v)。所以,頂點 v 的度 TD(v)= ID(v)+ OD(v)。
例如,在無向圖 (a)中有:
TD(v 1 )=2 TD(v 2 )=3 TD(v 3 )=2 TD(v 4 )=3 TD(v 5 )=2
在有向圖 (b)中有:
ID(v 1 )=1 OD(v 1 )=2 TD(v 1 )=3
ID(v 2 )=1 OD(v 2 )=2 TD(v 2 )=3
ID(v 3 )=2 OD(v 3 )=1 TD(v 3 )=3
ID(v 4 )=1 OD(v 4 )=2 TD(v 4 )=3
ID(v 5 )=2 OD(v 5 )=0 TD(v 5 )=2
7、權、網:有些圖的邊(或弧)附帶有一些數據信息,這些數據信息稱為邊(或弧)的權(Weight)。在實際問題中,權可以表示某種含義。比如,在一個地方的交通圖中,邊上的權值表示該條線路的長度或等級。在一個工程進度圖中,弧上的權值可以表示從前一個工程到后一個工程所需要的時間或其它代價等。邊(或弧)上帶權的圖稱為網或網絡(Network)。 下圖是權圖的示例圖。
8、子圖:設有兩個圖 G1=(V1,E1),G2=(V2,E2),如果 V1 是 V2 子集,E1 也是 E2 的子集,則稱圖 G1 是 G2 的子圖(Subgraph)。下圖是子圖的示例圖。
9、路徑、路徑長度:在無向圖G中,若存在一個頂點序列V p ,V i1 ,V i2 ,…,V im ,V q ,使得(V p ,V i1 ),(V i1 ,V i2 ),…,(V im ,V q )均屬於E(G)。則稱頂點V p 到V q 存在一條路徑(Path)。若G為有向圖,則路徑也是有向的。它由E(G)中的弧<V p ,V i1 >,<V i1 ,V i2 >,…,<V im ,V q >組成。路徑長度(Path Length)定義為路徑上邊或弧的數目。在圖 (a)交通網絡圖中,從頂點A到頂點B存在 4 條路徑,長度分別為1、2、2、4。在圖 (b)施工進度圖中,從頂點 1 到頂點 7 存在 2 條路徑,長度分別為 2 和3。
10、簡單路徑、回路、簡單回路:若一條路徑上頂點不重復出現,則稱此路徑為簡單路徑(Simple Path)。第一個頂點和最后一個頂點相同的路徑稱為回路(Cycle)或環。除第一個頂點和最后一個頂點相同其余頂點都不重復的回路稱為簡單回路(Simple Cycle),或者簡單環。
11、連通、連通圖、連通分量:在無向圖中,若兩個頂點之間有路徑,則稱這兩個頂點是連通的(Connect)。如果無向圖 G 中任意兩個頂點之間都是連通的,則稱圖 G 是連通圖(Connected Graph)。連通分量(Connected Compenent)是無向圖 G的極大連通子圖。極大連通子圖是一個圖的連通子圖,該子圖不是該圖的其它連通子圖的子圖。圖 (a)G1和圖(b)G2是連通圖,圖的連通分量的示例見下圖所示。圖 6.4(a)中的圖 G 有兩個連通分量。
12、強連通圖、強連通分量:在有向圖中,若圖中任意兩個頂點之間都存在從一個頂點到另一個頂點的路徑,則稱該有向圖是強連通圖(Strongly ConnectedGraph) 。 有 向 圖 的 極 大 強 連 通 子 圖 稱 為 強 連 通 分 量 (Strongly ConnectedComponent)。極大強連通子圖是一個有向圖的強連通子圖,該子圖不是該圖的其它強連通子圖的子圖。左圖是強連通圖,右圖是強連通分量的示例圖。圖(a)中的有向圖 G 有兩個強連通分量。
13、生成樹:所謂連通圖 G 的生成樹(Spanning Tree)是指 G 的包含其全部頂點的一個極小連通子圖。所謂極小連通子圖是指在包含所有頂點並且保證連通的前提下包含原圖中最少的邊。一棵具有 n 個頂點的連通圖 G 的生成樹有且僅有 n-1條邊,如果少一條邊就不是連通圖,如果多一條邊就一定有環。但是,有 n-1 條邊的圖不一定是生成樹。圖 6.7 就是圖 6.3(a)的一棵生成樹。
14、生成森林:在非連通圖中,由每個連通分量都可得到一個極小連通子圖,即一棵生成樹。這些連通分量的生成樹就組成了一個非連通圖的生成森林(Spanning Forest)。
1.1.3:圖的基本操作
圖的基本操作用一個接口來表示,為表示圖的基本操作,同時給出了頂點類的實現。由於頂點只保存自身信息,所以頂點類 Node<T>很簡單,里面只有一個字段 data。
頂點的類 Node<T>的實現如下所示。

1 public class Node<T> 2 { 3 private T data; //數據域 4 //構造器 5 public Node(T v) 6 { 7 data = v; 8 } 9 //數據域屬性 10 public T Data 11 { 12 get 13 { 14 return data; 15 } 16 set 17 { 18 data = value; 19 } 20 } 21 }
圖的接口IGraph<T>的定義如下所示。

1 public interface IGraph<T> 2 { 3 //獲取頂點數 4 int GetNumOfVertex(); 5 //獲取邊或弧的數目 6 int GetNumOfEdge(); 7 //在兩個頂點之間添加權為v的邊或弧 8 void SetEdge(Node<T> v1, Node<T> v2, int v); 9 //刪除兩個頂點之間的邊或弧 10 void DelEdge(Node<T> v1, Node<T> v2); 11 //判斷兩個頂點之間是否有邊或弧 12 bool IsEdge(Node<T> v1, Node<T> v2); 13 }
1.2:圖的存儲結構
圖是一種復雜的數據結構,頂點之間是多對多的關系,即任意兩個頂點之間都可能存在聯系。所以,無法以頂點在存儲區的位置關系來表示頂點之間的聯系,即順序存儲結構不能完全存儲圖的信息,但可以用數組來存儲圖的頂點信息。要存儲頂點之間的聯系必須用鏈式存儲結構或者二維數組。圖的存儲結構有多種,這里只介紹兩種基本的存儲結構:鄰接矩陣和鄰接表。
1.2.1: 鄰接矩陣
鄰接矩陣(Adjacency Matrix)是用兩個數組來表示圖,一個數組是一維數組,存儲圖中頂點的信息,一個數組是二維數組,即矩陣,存儲頂點之間相鄰的信息,也就是邊(或弧)的信息,這是鄰接矩陣名稱的由來。
假設圖G=(V, E)中有n個頂點,即V={v0,v1,…,vn-1},用矩陣A[i][j]表示邊(或弧)的信息。矩陣A[i][j]是一個n×n的矩陣,矩陣的元素為:
若 G 是網,則鄰接矩陣可定義為:
其中, wij 表示邊(vi,vj)或弧<vi,vj>上的權值;∞表示一個計算機允許的大於所有邊上權值的數。
下面三張圖分別對應鄰接矩陣如下:
從圖的鄰接矩陣表示法可以看出這種表示法的特點是:
(1)無向圖或無向網的鄰接矩陣一定是一個對稱矩陣。因此,在具體存放鄰接矩陣時只需存放上(或下)三角矩陣的元素即可。
(2)可以很方便地查找圖中任一頂點的度。對於無向圖或無向網而言,頂點 vi 的度就是鄰接矩陣中第 i 行或第 i 列中非 0 或非∞的元素的個數。對於有向圖或有向網而言,頂點 vi 的入度是鄰接矩陣中第 i 列中非 0 或非∞的元素的個數,頂點 vi 的出度是鄰接矩陣中第 i 行中非 0 或非∞的元素的個數。
(3)可以很方便地查找圖中任一條邊或弧的權值,只要 A[i][j]為 0 或∞,就說明頂點 vi 和 vj 之間不存在邊或弧。但是,要確定圖中有多少條邊或弧,則必須按行、按列對每個元素進行檢測,所花費的時間代價是很大的。這是用鄰接矩陣存儲圖的局限性。
無向圖鄰接矩陣類 GraphAdjMatrix<T>中有三個成員字段,一個是 Node<T>類型的一維數組 nodes,存放圖中的頂點信息;一個是整型的二維數組 matirx,表示圖的鄰接矩陣,存放邊的信息;一個是整數 numEdges,表示圖中邊的數目。因為圖的鄰接矩陣存儲結構對於確定圖中邊或弧的數目要花費很大的時間代價,所以設了這個字段。
無向圖鄰接矩陣類 GraphAdjMatrix<T>的實現如下所示。

1 /// <summary> 2 /// 無向圖鄰接矩陣類 GraphAdjMatrix<T>的實現 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class GraphAdjMatrix<T> : IGraph<T> 6 { 7 private Node<T>[] nodes;//頂點數組 8 private int[,] matrix;//鄰接矩陣數組 9 private int numEdges;//邊的數目 10 11 public GraphAdjMatrix(int n) 12 { 13 nodes = new Node<T>[n]; 14 matrix = new int[n, n]; 15 numEdges = 0; 16 } 17 18 //獲取索引為index的頂點的信息 19 public Node<T> GetNode(int index) 20 { 21 return nodes[index]; 22 } 23 24 //設置索引為index的頂點的信息 25 public void SetNode(int index, Node<T> v) 26 { 27 nodes[index] = v; 28 } 29 30 //獲取matrix[index1, index2]的值 31 public int GetMatrix(int index1, int index2) 32 { 33 return matrix[index1, index2]; 34 } 35 36 //設置matrix[index1, index2]的值 37 public void SetMatrix(int index1, int index2) 38 { 39 matrix[index1, index2] = 1; 40 } 41 42 //邊的數目屬性 43 public int NumEdges 44 { 45 get { return numEdges; } 46 set { numEdges = value; } 47 } 48 49 //獲取頂點的數目 50 public int GetNumOfVertex() 51 { 52 return nodes.Length; 53 } 54 55 //獲取邊的數目 56 public int GetNumOfEdge() 57 { 58 return numEdges; 59 } 60 61 //判斷v是否是圖的頂點 62 public bool IsNode(Node<T> v) 63 { 64 //遍歷頂點數組 65 foreach (Node<T> nd in nodes) 66 { 67 //如果頂點nd與v相等,則v是圖的頂點,返回true 68 if (v.Equals(nd)) 69 { 70 return true; 71 } 72 } 73 74 return false; 75 } 76 77 //獲取頂點v在頂點數組中的索引 78 public int GetIndex(Node<T> v) 79 { 80 int i = -1; 81 //遍歷頂點數組 82 for (i = 0; i < nodes.Length; ++i) 83 { 84 //如果頂點v與nodes[i]相等,則v是圖的頂點,返回索引值i。 85 if (nodes[i].Equals(v)) 86 { 87 return i; 88 } 89 } 90 return i; 91 } 92 93 //在頂點v1和v2之間添加權值為v的邊 94 public void SetEdge(Node<T> v1, Node<T> v2, int v) 95 { 96 if (!IsNode(v1) || !IsNode(v2)) 97 { 98 Console.WriteLine("Node is not belong to Graph!"); 99 return; 100 } 101 102 //不是無向圖 103 if (v != 1) 104 { 105 Console.WriteLine("Weight is not right!"); 106 return; 107 } 108 109 //矩陣是對稱矩陣 110 matrix[GetIndex(v1), GetIndex(v2)] = v; 111 matrix[GetIndex(v2), GetIndex(v1)] = v; 112 ++numEdges; 113 } 114 115 //刪除頂點v1和v2之間的邊 116 public void DelEdge(Node<T> v1, Node<T> v2) 117 { 118 //v1或v2不是圖的頂點 119 if (!IsNode(v1) || !IsNode(v2)) 120 { 121 Console.WriteLine("Node is not belong to Graph!"); 122 return; 123 } 124 125 //頂點v1與v2之間存在邊 126 if (matrix[GetIndex(v1), GetIndex(v2)] == 1) 127 { 128 //矩陣是對稱矩陣 129 matrix[GetIndex(v1), GetIndex(v2)] = 0; 130 matrix[GetIndex(v2), GetIndex(v1)] = 0; 131 --numEdges; 132 } 133 } 134 135 //判斷頂點v1與v2之間是否存在邊 136 public bool IsEdge(Node<T> v1, Node<T> v2) 137 { 138 //v1或v2不是圖的頂點 139 if (!IsNode(v1) || !IsNode(v2)) 140 { 141 Console.WriteLine("Node is not belong to Graph!"); 142 return false; 143 } 144 145 //頂點v1與v2之間存在邊 146 if (matrix[GetIndex(v1), GetIndex(v2)] == 1) 147 { 148 return true; 149 } 150 else //不存在邊 151 { 152 return false; 153 } 154 } 155 156 }
1.2.2: 鄰接表
鄰接表(Adjacency List)是圖的一種順序存儲與鏈式存儲相結合的存儲結構,類似於樹的孩子鏈表表示法。順序存儲指的是圖中的頂點信息用一個頂點數組來存儲,一個頂點數組元素是一個頂點結點,頂點結點有兩個域,一個是數據域data,存放與頂點相關的信息,一個是引用域 firstAdj,存放該頂點的鄰接表的第一個結點的地址。頂點的鄰接表是把所有鄰接於某頂點的頂點構成的一個表,它是采用鏈式存儲結構。所以,我們說鄰接表是圖的一種順序存儲與鏈式存儲相結合的存儲結構。其中,鄰接表中的每個結點實際上保存的是與該頂點相關的邊或弧的信息,它有兩個域,一個是鄰接頂點域 adjvex,存放鄰接頂點的信息,實際上就是鄰接頂點在頂點數組中的序號;一個是引用域 next,存放下一個鄰接頂點的結點的地址。頂點結點和鄰接表結點的結構如下圖所示。
而對於網的鄰接表結點還需要存儲邊上的信息(如權值),所以結點應增設 一個域 info。網的鄰接表結點的結構如下圖 所示。
圖 (a)的鄰接表如下圖 所示。
若無向圖中有 n 個頂點和 e 條邊,則它的鄰接表需 n 個頂點結點和 2e 個鄰接表結點,在邊稀疏 的情況下,用鄰接表存儲圖比用鄰接矩陣節省存儲空間,當與邊相關的信息較多時更是如此。
在無向圖的鄰接表中,頂點 vi 的度恰為第 i 個鄰接表中的結點數;而在有向圖中,第 i 的鄰接表中的結點數只是頂點 vi 的出度,為求入度,必須遍歷整個鄰接表。在所有鄰接表中其鄰接頂點域的值為 i 的結點的個數是頂點 vi 的入度。有時,為了便於確定頂點的入度或者以頂點 vi 為頭的弧,可以建立一個有向圖的逆鄰接表,即對每個頂點 vi 建立一個以 vi 為頭的弧的鄰接表。下圖(b)是(a)鄰接表和(b)逆鄰接表。
在建立鄰接表或逆鄰接表時,若輸入的頂點信息即為頂點的編號,則建立鄰接表的時間復雜度為 O(n+e),否則,需要查找才能得到頂點在圖中的位置,則時間復雜度為 O(n*e)。
在鄰接表上很容易找到任一頂點的第一個鄰接點和下一個鄰接點。但要判定任意兩個頂點(vi 和 v j)之間是否有邊或弧相連,則需查找第 i 個或 j 個鄰接表,因此,不如鄰接矩陣方便。
下面以無向圖鄰接表類的實現來說明圖的鄰接表類的實現。
無向圖鄰接表的鄰接表結點類 adjListNode<T>有兩個成員字段,一個是adjvex,存儲鄰接頂點的信息,類型是整型;一個是 next,存儲下一個鄰接表結點的地址,類型是 adjListNode<T>。 adjListNode<T>的實現如下所示。

1 /// <summary> 2 /// 無向圖鄰接表類的實現 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class adjListNode<T> 6 { 7 private int adjvex;//鄰接頂點 8 private adjListNode<T> next;//下一個鄰接表結點 9 10 //鄰接頂點屬性 11 public int Adjvex 12 { 13 get { return adjvex; } 14 set { adjvex = value; } 15 } 16 17 //下一個鄰接表結點屬性 18 public adjListNode<T> Next 19 { 20 get { return next; } 21 set { next = value; } 22 } 23 24 public adjListNode(int vex) 25 { 26 adjvex = vex; 27 next = null; 28 } 29 }
無向圖鄰接表的頂點結點類 VexNode<T>有兩個成員字段,一個 data,它存儲圖的頂點本身的信息,類型是 Node<T>;一個是 firstAdj, 存儲頂點的鄰接表的第 1 個結點的地址,類型是 adjListNode<T>。 VexNode<T>的實現如下所示。

1 /// <summary> 2 /// 無向圖鄰接表的頂點結點類 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class VexNode<T> 6 { 7 private Node<T> data; //圖的頂點 8 private adjListNode<T> firstAdj; //鄰接表的第1個結點 9 10 public Node<T> Data 11 { 12 get { return data; } 13 set { data = value; } 14 } 15 16 //鄰接表的第1個結點屬性 17 public adjListNode<T> FirstAdj 18 { 19 get { return firstAdj; } 20 set { firstAdj = value; } 21 } 22 23 //構造器 24 public VexNode() 25 { 26 data = null; 27 firstAdj = null; 28 } 29 30 //構造器 31 public VexNode(Node<T> nd) 32 { 33 data = nd; 34 firstAdj = null; 35 } 36 37 //構造器 38 public VexNode(Node<T> nd, adjListNode<T> alNode) 39 { 40 data = nd; 41 firstAdj = alNode; 42 } 43 }
無向圖鄰接表類 GraphAdjList<T>有一個成員字段 adjList, 表示鄰接表數組,數組元素的類型是 VexNode<T>。 GraphAdjList<T>實現了接口 IGraph<T>中的方法。與無向圖鄰接矩陣類 GraphAdjMatrix<T>一樣, GraphAdjList<T>實現了兩個成員方法 IsNode 和 GetIndex。功能與 GraphAdjMatrix<T>一樣。無向圖鄰接表類 GraphAdjList<T>的實現如下所示。

1 /// <summary> 2 /// 無向圖鄰接表類 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class GraphAdjList<T> : IGraph<T> 6 { 7 //鄰接表數組 8 private VexNode<T>[] adjList; 9 10 //索引器 11 public VexNode<T> this[int index] 12 { 13 get 14 { 15 return adjList[index]; 16 } 17 set 18 { 19 adjList[index] = value; 20 } 21 } 22 23 //構造器 24 public GraphAdjList(Node<T>[] nodes) 25 { 26 adjList = new VexNode<T>[nodes.Length]; 27 for (int i = 0; i < nodes.Length; ++i) 28 { 29 adjList[i].Data = nodes[i]; 30 adjList[i].FirstAdj = null; 31 } 32 } 33 34 //獲取頂點的數目 35 public int GetNumOfVertex() 36 { 37 return adjList.Length; 38 } 39 40 //獲取邊的數目 41 public int GetNumOfEdge() 42 { 43 int i = 0; 44 45 foreach (VexNode<T> nd in adjList) 46 { 47 adjListNode<T> p = nd.FirstAdj; 48 while (p != null) 49 { 50 ++i; 51 p = p.Next; 52 } 53 } 54 55 return i / 2;//無向圖 56 } 57 58 //判斷v是否是圖的頂點 59 public bool IsNode(Node<T> v) 60 { 61 //遍歷鄰接表數組 62 foreach (VexNode<T> nd in adjList) 63 { 64 //如果v等於nd的data,則v是圖中的頂點,返回true 65 if (v.Equals(nd.Data)) 66 { 67 return true; 68 } 69 } 70 return false; 71 } 72 73 //獲取頂點v在鄰接表數組中的索引 74 public int GetIndex(Node<T> v) 75 { 76 int i = -1; 77 //遍歷鄰接表數組 78 for (i = 0; i < adjList.Length; ++i) 79 { 80 //鄰接表數組第i項的data值等於v,則頂點v的索引為i 81 if (adjList[i].Data.Equals(v)) 82 { 83 return i; 84 } 85 } 86 return i; 87 } 88 89 //判斷v1和v2之間是否存在邊 90 public bool IsEdge(Node<T> v1, Node<T> v2) 91 { 92 //v1或v2不是圖的頂點 93 if (!IsNode(v1) || !IsNode(v2)) 94 { 95 Console.WriteLine("Node is not belong to Graph!"); 96 return false; 97 } 98 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 99 while (p != null) 100 { 101 if (p.Adjvex == GetIndex(v2)) 102 { 103 return true; 104 } 105 106 p = p.Next; 107 } 108 109 return false; 110 } 111 112 //在頂點v1和v2之間添加權值為v的邊 113 public void SetEdge(Node<T> v1, Node<T> v2, int v) 114 { 115 //v1或v2不是圖的頂點或者v1和v2之間存在邊 116 if (!IsNode(v1) || !IsNode(v2) || IsEdge(v1, v2)) 117 { 118 Console.WriteLine("Node is not belong to Graph!"); 119 return; 120 } 121 122 //權值不對 123 if (v != 1) 124 { 125 Console.WriteLine("Weight is not right!"); 126 return; 127 } 128 129 //處理頂點v1的鄰接表 130 adjListNode<T> p = new adjListNode<T>(GetIndex(v2)); 131 132 if (adjList[GetIndex(v1)].FirstAdj == null) 133 { 134 adjList[GetIndex(v1)].FirstAdj = p; 135 } 136 //頂點v1有鄰接頂點 137 else 138 { 139 p.Next = adjList[GetIndex(v1)].FirstAdj; 140 adjList[GetIndex(v1)].FirstAdj = p; 141 } 142 143 //處理頂點v2的鄰接表 144 p = new adjListNode<T>(GetIndex(v1)); 145 146 //頂點v2沒有鄰接頂點 147 if (adjList[GetIndex(v2)].FirstAdj == null) 148 { 149 adjList[GetIndex(v2)].FirstAdj = p; 150 } 151 152 //頂點v2有鄰接頂點 153 else 154 { 155 p.Next = adjList[GetIndex(v2)].FirstAdj; 156 adjList[GetIndex(v2)].FirstAdj = p; 157 } 158 } 159 160 //刪除頂點v1和v2之間的邊 161 public void DelEdge(Node<T> v1, Node<T> v2) 162 { 163 //v1或v2不是圖的頂點 164 if (!IsNode(v1) || !IsNode(v2)) 165 { 166 Console.WriteLine("Node is not belong to Graph!"); 167 return; 168 } 169 170 //頂點v1與v2之間有邊 171 if (IsEdge(v1, v2)) 172 { 173 //處理頂點v1的鄰接表中的頂點v2的鄰接表結點 174 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 175 adjListNode<T> pre = null; 176 177 while (p != null) 178 { 179 if (p.Adjvex != GetIndex(v2)) 180 { 181 pre = p; 182 p = p.Next; 183 } 184 } 185 pre.Next = p.Next; 186 187 //處理頂點v2的鄰接表中的頂點v1的鄰接表結點 188 p = adjList[GetIndex(v2)].FirstAdj; 189 pre = null; 190 191 while (p != null) 192 { 193 if (p.Adjvex != GetIndex(v1)) 194 { 195 pre = p; 196 p = p.Next; 197 } 198 } 199 pre.Next = p.Next; 200 } 201 } 202 }
1.3:圖的遍歷
圖的遍歷是指從圖中的某個頂點出發,按照某種順序訪問圖中的每個頂點,使每個頂點被訪問一次且僅一次。圖的遍歷與樹的遍歷操作功能相似。圖的遍歷是圖的一種基本操作,圖的許多其他操作都是建立在遍歷操作的基礎之上的。
然而,圖的遍歷要比樹的遍歷復雜得多。這是因為圖中的頂點之間是多對多的關系,圖中的任何一個頂點都可能和其它的頂點相鄰接。所以,在訪問了某個頂點之后,從該頂點出發,可能沿着某條路徑遍歷之后,又回到該頂點上。例如,在圖 (b)的圖中,由於圖中存在回路,因此在訪問了 v1、 v3、 v4 之后,沿着邊<v4, v1>又回到了 v1 上。為了避免同一頂點被訪問多次,在遍歷圖的過程中,必須記下每個已訪問過的頂點。為此,可以設一個輔助數組 visited[n], n 為圖中頂點的數目。數組中元素的初始值全為 0,表示頂點都沒有被訪問過,如果頂點vi 被訪問, visited[i-1]為 1。
圖的遍歷有深度優先遍歷和廣度優先遍歷兩種方式,它們對圖和網都適用。
1.3.1:深度優先遍歷(DFS)
圖的深度優先遍歷(Depth_First Search)類似於樹的先序遍歷,是樹的先序遍歷的推廣。
假設初始狀態是圖中所有頂點未曾被訪問過,則深度優先遍歷可從圖中某個頂點 v 出發,訪問此頂點,然后依次從 v 的未被訪問的鄰接頂點出發深度優先遍歷圖,直至圖中所有和 v 有路徑相通的頂點都被遍歷過。若此時圖中尚有未被訪問的頂點,則另選圖中一個未被訪問的頂點作為起始點,重復上述過程,直到圖中所有頂點都被訪問到為止。
按深度優先遍歷算法對圖 (a)進行遍歷。
圖 6.13(a)所示的無向圖的深度優先遍歷的過程如圖 6.13(b)所示。假設從頂點 v1 出發,在訪問了頂點 v1 之后,選擇鄰接頂點 v2,因為 v2 未被訪問過,所以從 v2 出發進行深度優先遍歷。依次類推,接着從 v4、 v8、 v5 出發進行深度優先遍歷。當訪問了 v5 之后,由於 v5 的鄰接頂點 v2 和 v8 都已被訪問,所以遍歷退回到 v8。由於同樣的理由,遍歷繼續退回到 v4、 v2 直到 v1。由於 v1 的另一個鄰接頂點 v3 未被訪問,所以又從 v3 開始進行深度優先遍歷,這樣得到該圖的深度優先遍歷的頂點序列為 v1→v2→v4→v8→v5→v3→v6→v7。
顯然,這是一個遞歸的過程。下面以無向圖的鄰接表存儲結構為例來實現圖的深度優先遍歷算法。在類中增設了一個整型數組的成員字段 visited,它的初始值全為 0,表示圖中所有的頂點都沒有被訪問過。如果頂點 vi 被訪問, visited[i-1]為 1。並且,把該算法作為無向圖的鄰接表類 GraphAdjList<T>的成員方法。
對無向圖的鄰接表類 GraphAdjList<T>進行修改后的算法實現如下:

1 /// <summary> 2 /// 無向圖鄰接表類 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class GraphAdjList<T> : IGraph<T> 6 { 7 //鄰接表數組 8 private VexNode<T>[] adjList; 9 10 //記錄圖中所有頂點的訪問情況 11 private int[] visited; 12 13 //索引器 14 public VexNode<T> this[int index] 15 { 16 get 17 { 18 return adjList[index]; 19 } 20 set 21 { 22 adjList[index] = value; 23 } 24 } 25 26 //構造器 27 public GraphAdjList(Node<T>[] nodes) 28 { 29 adjList = new VexNode<T>[nodes.Length]; 30 for (int i = 0; i < nodes.Length; ++i) 31 { 32 adjList[i].Data = nodes[i]; 33 adjList[i].FirstAdj = null; 34 } 35 36 visited = new int[adjList.Length]; 37 for (int i = 0; i < visited.Length; ++i) 38 { 39 visited[i] = 0; 40 } 41 } 42 43 //獲取頂點的數目 44 public int GetNumOfVertex() 45 { 46 return adjList.Length; 47 } 48 49 //獲取邊的數目 50 public int GetNumOfEdge() 51 { 52 int i = 0; 53 54 foreach (VexNode<T> nd in adjList) 55 { 56 adjListNode<T> p = nd.FirstAdj; 57 while (p != null) 58 { 59 ++i; 60 p = p.Next; 61 } 62 } 63 64 return i / 2;//無向圖 65 } 66 67 //判斷v是否是圖的頂點 68 public bool IsNode(Node<T> v) 69 { 70 //遍歷鄰接表數組 71 foreach (VexNode<T> nd in adjList) 72 { 73 //如果v等於nd的data,則v是圖中的頂點,返回true 74 if (v.Equals(nd.Data)) 75 { 76 return true; 77 } 78 } 79 return false; 80 } 81 82 //獲取頂點v在鄰接表數組中的索引 83 public int GetIndex(Node<T> v) 84 { 85 int i = -1; 86 //遍歷鄰接表數組 87 for (i = 0; i < adjList.Length; ++i) 88 { 89 //鄰接表數組第i項的data值等於v,則頂點v的索引為i 90 if (adjList[i].Data.Equals(v)) 91 { 92 return i; 93 } 94 } 95 return i; 96 } 97 98 //判斷v1和v2之間是否存在邊 99 public bool IsEdge(Node<T> v1, Node<T> v2) 100 { 101 //v1或v2不是圖的頂點 102 if (!IsNode(v1) || !IsNode(v2)) 103 { 104 Console.WriteLine("Node is not belong to Graph!"); 105 return false; 106 } 107 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 108 while (p != null) 109 { 110 if (p.Adjvex == GetIndex(v2)) 111 { 112 return true; 113 } 114 115 p = p.Next; 116 } 117 118 return false; 119 } 120 121 //在頂點v1和v2之間添加權值為v的邊 122 public void SetEdge(Node<T> v1, Node<T> v2, int v) 123 { 124 //v1或v2不是圖的頂點或者v1和v2之間存在邊 125 if (!IsNode(v1) || !IsNode(v2) || IsEdge(v1, v2)) 126 { 127 Console.WriteLine("Node is not belong to Graph!"); 128 return; 129 } 130 131 //權值不對 132 if (v != 1) 133 { 134 Console.WriteLine("Weight is not right!"); 135 return; 136 } 137 138 //處理頂點v1的鄰接表 139 adjListNode<T> p = new adjListNode<T>(GetIndex(v2)); 140 141 if (adjList[GetIndex(v1)].FirstAdj == null) 142 { 143 adjList[GetIndex(v1)].FirstAdj = p; 144 } 145 //頂點v1有鄰接頂點 146 else 147 { 148 p.Next = adjList[GetIndex(v1)].FirstAdj; 149 adjList[GetIndex(v1)].FirstAdj = p; 150 } 151 152 //處理頂點v2的鄰接表 153 p = new adjListNode<T>(GetIndex(v1)); 154 155 //頂點v2沒有鄰接頂點 156 if (adjList[GetIndex(v2)].FirstAdj == null) 157 { 158 adjList[GetIndex(v2)].FirstAdj = p; 159 } 160 161 //頂點v2有鄰接頂點 162 else 163 { 164 p.Next = adjList[GetIndex(v2)].FirstAdj; 165 adjList[GetIndex(v2)].FirstAdj = p; 166 } 167 } 168 169 //刪除頂點v1和v2之間的邊 170 public void DelEdge(Node<T> v1, Node<T> v2) 171 { 172 //v1或v2不是圖的頂點 173 if (!IsNode(v1) || !IsNode(v2)) 174 { 175 Console.WriteLine("Node is not belong to Graph!"); 176 return; 177 } 178 179 //頂點v1與v2之間有邊 180 if (IsEdge(v1, v2)) 181 { 182 //處理頂點v1的鄰接表中的頂點v2的鄰接表結點 183 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 184 adjListNode<T> pre = null; 185 186 while (p != null) 187 { 188 if (p.Adjvex != GetIndex(v2)) 189 { 190 pre = p; 191 p = p.Next; 192 } 193 } 194 pre.Next = p.Next; 195 196 //處理頂點v2的鄰接表中的頂點v1的鄰接表結點 197 p = adjList[GetIndex(v2)].FirstAdj; 198 pre = null; 199 200 while (p != null) 201 { 202 if (p.Adjvex != GetIndex(v1)) 203 { 204 pre = p; 205 p = p.Next; 206 } 207 } 208 pre.Next = p.Next; 209 } 210 } 211 212 //無向圖的深度優先遍歷算法的實現 213 public void DFS() 214 { 215 for (int i = 0; i < visited.Length; ++i) 216 { 217 if (visited[i] == 0) 218 { 219 DFSAL(i); 220 } 221 } 222 } 223 224 //從某個頂點出發進行深度優先遍歷 225 public void DFSAL(int i) 226 { 227 visited[i] = 1; 228 adjListNode<T> p = adjList[i].FirstAdj; 229 while (p != null) 230 { 231 if (visited[p.Adjvex] == 0) 232 { 233 DFSAL(p.Adjvex); 234 } 235 p = p.Next; 236 } 237 238 } 239 }
分析上面的算法,在遍歷圖時,對圖中每個頂點至多調用一次DFS方法,因為一旦某個頂點被標記成已被訪問,就不再從它出發進行遍歷。因此,遍歷圖的過程實質上是對每個頂點查找其鄰接頂點的過程。其時間復雜度取決於所采用的存儲結構。當圖采用鄰接矩陣作為存儲結構時,查找每個頂點的鄰接頂點的時間復雜度為O(n2),其中,n為圖的頂點數。而以鄰接表作為圖的存儲結構時,查找鄰接頂點的時間復雜度為O(e),其中,e為圖中邊或弧的數目。因此,當以鄰接表作為存儲結構時,深度優先遍歷圖的時間復雜度為O(n+e)。
1.3.2:廣度優先遍歷(BFS)
圖的廣度優先遍歷(Breadth_First Search)類似於樹的層序遍歷。
假設從圖中的某個頂點 v 出發,訪問了 v 之后,依次訪問 v 的各個未曾訪問的鄰接頂點。然后分別從這些鄰接頂點出發依次訪問它們的鄰接頂點,並使“先被訪問的頂點的鄰接頂點”先於“后被訪問的頂點的鄰接頂點”被訪問,直至圖中所有已被訪問的頂點的鄰接頂點都被訪問。若此時圖中尚有頂點未被訪問,則另選圖中未被訪問的頂點作為起點,重復上述過程,直到圖中所有的頂點都被訪問為止。換句話說,廣度優先遍歷圖的過程是以某個頂點 v 作為起始點,由近至遠,依次訪問和 v 有路徑相通且路徑長度為 1, 2,…的頂點。
按廣度優先遍歷算法對上圖 (a)進行遍歷
上圖 (a)所示的無向圖的廣度優先遍歷的過程如上圖 (c)所示。假設從頂點 v1 開始進行廣度優先遍歷,首先訪問頂點 v1 和它的鄰接頂點 v2 和 v3,然后依次訪問 v2 的鄰接頂點 v4 和 v5,以及 v3 的鄰接頂點 v6 和 v7,最后訪問 v4的鄰接頂點 v8。由於這些頂點的鄰接頂點都已被訪問,並且圖中所有頂點都已被訪問,由此完成了圖的遍歷,得到的頂點訪問序列為: v1→v2→v3→v4→v5→v6→v7→v8,其遍歷過程如圖 (c)所示。
和深度優先遍歷類似,在廣度優先遍歷中也需要一個訪問標記數組,我們采用與深度優先遍歷同樣的數組。並且,為了順序訪問路徑長度為 1, 2,…的頂點,需在算法中附設一個隊列來存儲已被訪問的路徑長度為 1, 2,…的頂點。
以鄰接表作為存儲結構的無向圖的廣度優先遍歷算法的實現如下,隊列是循環順序隊列。
對無向圖的鄰接表類 GraphAdjList<T>進行修改后的算法實現如下:

1 /// <summary> 2 /// 無向圖鄰接表類 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class GraphAdjList<T> : IGraph<T> 6 { 7 //鄰接表數組 8 private VexNode<T>[] adjList; 9 10 //記錄圖中所有頂點的訪問情況 11 private int[] visited; 12 13 //索引器 14 public VexNode<T> this[int index] 15 { 16 get 17 { 18 return adjList[index]; 19 } 20 set 21 { 22 adjList[index] = value; 23 } 24 } 25 26 //構造器 27 public GraphAdjList(Node<T>[] nodes) 28 { 29 adjList = new VexNode<T>[nodes.Length]; 30 for (int i = 0; i < nodes.Length; ++i) 31 { 32 adjList[i].Data = nodes[i]; 33 adjList[i].FirstAdj = null; 34 } 35 36 visited = new int[adjList.Length]; 37 for (int i = 0; i < visited.Length; ++i) 38 { 39 visited[i] = 0; 40 } 41 } 42 43 //獲取頂點的數目 44 public int GetNumOfVertex() 45 { 46 return adjList.Length; 47 } 48 49 //獲取邊的數目 50 public int GetNumOfEdge() 51 { 52 int i = 0; 53 54 foreach (VexNode<T> nd in adjList) 55 { 56 adjListNode<T> p = nd.FirstAdj; 57 while (p != null) 58 { 59 ++i; 60 p = p.Next; 61 } 62 } 63 64 return i / 2;//無向圖 65 } 66 67 //判斷v是否是圖的頂點 68 public bool IsNode(Node<T> v) 69 { 70 //遍歷鄰接表數組 71 foreach (VexNode<T> nd in adjList) 72 { 73 //如果v等於nd的data,則v是圖中的頂點,返回true 74 if (v.Equals(nd.Data)) 75 { 76 return true; 77 } 78 } 79 return false; 80 } 81 82 //獲取頂點v在鄰接表數組中的索引 83 public int GetIndex(Node<T> v) 84 { 85 int i = -1; 86 //遍歷鄰接表數組 87 for (i = 0; i < adjList.Length; ++i) 88 { 89 //鄰接表數組第i項的data值等於v,則頂點v的索引為i 90 if (adjList[i].Data.Equals(v)) 91 { 92 return i; 93 } 94 } 95 return i; 96 } 97 98 //判斷v1和v2之間是否存在邊 99 public bool IsEdge(Node<T> v1, Node<T> v2) 100 { 101 //v1或v2不是圖的頂點 102 if (!IsNode(v1) || !IsNode(v2)) 103 { 104 Console.WriteLine("Node is not belong to Graph!"); 105 return false; 106 } 107 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 108 while (p != null) 109 { 110 if (p.Adjvex == GetIndex(v2)) 111 { 112 return true; 113 } 114 115 p = p.Next; 116 } 117 118 return false; 119 } 120 121 //在頂點v1和v2之間添加權值為v的邊 122 public void SetEdge(Node<T> v1, Node<T> v2, int v) 123 { 124 //v1或v2不是圖的頂點或者v1和v2之間存在邊 125 if (!IsNode(v1) || !IsNode(v2) || IsEdge(v1, v2)) 126 { 127 Console.WriteLine("Node is not belong to Graph!"); 128 return; 129 } 130 131 //權值不對 132 if (v != 1) 133 { 134 Console.WriteLine("Weight is not right!"); 135 return; 136 } 137 138 //處理頂點v1的鄰接表 139 adjListNode<T> p = new adjListNode<T>(GetIndex(v2)); 140 141 if (adjList[GetIndex(v1)].FirstAdj == null) 142 { 143 adjList[GetIndex(v1)].FirstAdj = p; 144 } 145 //頂點v1有鄰接頂點 146 else 147 { 148 p.Next = adjList[GetIndex(v1)].FirstAdj; 149 adjList[GetIndex(v1)].FirstAdj = p; 150 } 151 152 //處理頂點v2的鄰接表 153 p = new adjListNode<T>(GetIndex(v1)); 154 155 //頂點v2沒有鄰接頂點 156 if (adjList[GetIndex(v2)].FirstAdj == null) 157 { 158 adjList[GetIndex(v2)].FirstAdj = p; 159 } 160 161 //頂點v2有鄰接頂點 162 else 163 { 164 p.Next = adjList[GetIndex(v2)].FirstAdj; 165 adjList[GetIndex(v2)].FirstAdj = p; 166 } 167 } 168 169 //刪除頂點v1和v2之間的邊 170 public void DelEdge(Node<T> v1, Node<T> v2) 171 { 172 //v1或v2不是圖的頂點 173 if (!IsNode(v1) || !IsNode(v2)) 174 { 175 Console.WriteLine("Node is not belong to Graph!"); 176 return; 177 } 178 179 //頂點v1與v2之間有邊 180 if (IsEdge(v1, v2)) 181 { 182 //處理頂點v1的鄰接表中的頂點v2的鄰接表結點 183 adjListNode<T> p = adjList[GetIndex(v1)].FirstAdj; 184 adjListNode<T> pre = null; 185 186 while (p != null) 187 { 188 if (p.Adjvex != GetIndex(v2)) 189 { 190 pre = p; 191 p = p.Next; 192 } 193 } 194 pre.Next = p.Next; 195 196 //處理頂點v2的鄰接表中的頂點v1的鄰接表結點 197 p = adjList[GetIndex(v2)].FirstAdj; 198 pre = null; 199 200 while (p != null) 201 { 202 if (p.Adjvex != GetIndex(v1)) 203 { 204 pre = p; 205 p = p.Next; 206 } 207 } 208 pre.Next = p.Next; 209 } 210 } 211 212 //無向圖的深度優先遍歷算法的實現 213 public void DFS() 214 { 215 for (int i = 0; i < visited.Length; ++i) 216 { 217 if (visited[i] == 0) 218 { 219 DFSAL(i); 220 } 221 } 222 } 223 224 //從某個頂點出發進行深度優先遍歷 225 public void DFSAL(int i) 226 { 227 visited[i] = 1; 228 adjListNode<T> p = adjList[i].FirstAdj; 229 while (p != null) 230 { 231 if (visited[p.Adjvex] == 0) 232 { 233 DFSAL(p.Adjvex); 234 } 235 p = p.Next; 236 } 237 238 } 239 240 //無向圖的廣度優先遍歷算法的實現 241 public void BFS() 242 { 243 for (int i = 0; i < visited.Length; ++i) 244 { 245 if (visited[i] == 0) 246 { 247 BFSAL(i); 248 } 249 } 250 } 251 252 //從某個頂點出發進行廣度優先遍歷 253 public void BFSAL(int i) 254 { 255 visited[i] = 1; 256 CSeqQueue<int> cq = new CSeqQueue<int>(visited.Length); 257 cq.In(i); 258 while (!cq.IsEmpty()) 259 { 260 int k = cq.Out(); 261 adjListNode<T> p = adjList[k].FirstAdj; 262 263 while(p != null) 264 { 265 if (visited[p.Adjvex] == 0) 266 { 267 visited[p.Adjvex] = 1; 268 cq.In(p.Adjvex); 269 } 270 271 p = p.Next; 272 } 273 } 274 } 275 }
分析上面的算法,每個頂點至多入隊列一次。遍歷圖的過程實質上是通過邊或弧查找鄰接頂點的過程,因此,廣度優先遍歷算法的時間復雜度與深度優先遍歷相同,兩者的不同之處在於對頂點的訪問順序不同。
1.4:圖的應用
1.4.1:最小生成樹
1、 最小生成樹的基本概念
由生成樹的定義可知,無向連通圖的生成樹不是唯一的,對連通圖的不同遍歷就得到不同的生成樹。下圖所示是圖 (a)所示的無向連通圖的部分生成樹。
如果是一個無向連通網,那么它的所有生成樹中必有一棵邊的權值總和最小的生成樹,我們稱這棵生成樹為最小代價生成樹(Minimum Cost Spanning Tree),簡稱最小生成樹。
許多應用問題都是一個求無向連通網的最小生成樹問題。例如,要在 n 個城市之間鋪設光纜,鋪設光纜的費用很高,並且各個城市之間鋪設光纜的費用不同。一個目標是要使這 n 個城市的任意兩個之間都可以直接或間接通信,另一個目標是要使鋪設光纜的總費用最低。如果把 n 個城市看作是圖的 n 個頂點,兩個城市之間鋪設的光纜看作是兩個頂點之間的邊,這實際上就是求一個無向連通網的最小生成樹問題。
由最小生成樹的定義可知,構造有 n 個頂點的無向連通網的最小生成樹必須滿足以下三個條件:
(1)構造的最小生成樹必須包括 n 個頂點;
(2)構造的最小生成樹有且僅有 n-1 條邊;
(3)構造的最小生成樹中不存在回路。
構造最小生成樹的方法有許多種,典型的方法有兩種,一種是普里姆(Prim)算法,一種是克魯斯卡爾(Kruskal)算法。
2、 普里姆(Prim)算法
假設 G=(V, E)為一無向連通網,其中, V 為網中頂點的集合, E 為網中邊的集合。設置兩個新的集合 U 和 T,其中, U 為 G 的最小生成樹的頂點的集合, T 為 G 的最小生成樹的邊的集合。普里姆算法的思想是:令集合 U 的初值為 U={u1}(假設構造最小生成樹時從頂點 u1 開始),集合 T 的初值為 T={}。從所有的頂點 u∈U 和頂點 v∈V-U 的帶權邊中選出具有最小權值的邊(u,v),將頂點 v 加入集合 U 中,將邊(u,v)加入集合 T 中。如此不斷地重復直到 U=V 時,最小生成樹構造完畢。此時,集合 U 中存放着最小生成樹的所有頂點,集合 T中存放着最小生成樹的所有邊。
以下圖(a)為例,說明用普里姆算法構造圖的無向連通網的最小生成樹的過程。
為了分析問題的方便,把圖 (a)中所示的無向連通網重新畫在下圖 中,如圖 (a)所示。初始時,算法的集合 U={A},集合 V-U={B,C,D,E},集合 T={},如圖 (b)所示。在所有 u 為集合 U 中頂點、 v 為集合 V-U 中頂點的邊(u,v)中尋找具有最小權值的邊,尋找到的邊是(A,D),權值為 20,把頂點 B 加入到集合U 中,把邊(A,D)加入到集合 T 中,如圖 (c)所示。在所有 u 為集合 U 中頂點、v 為集合 V-U 中頂點的邊(u,v)中尋找具有最小權值的邊,尋找到的邊是(D,E),權值為 10,把頂點 E 加入到集合 U 中,把邊(D,E)加入到集合 T 中,如圖 (d)所示。隨后依次從集合 V-U 中加入到集合 U 中的頂點為 B、 C,依次加入到集合T 中的邊為(A,B)(權值為 60)、 (E,C) (權值為 70),分別如圖 (e)、 (f)所示。最后得到的圖 (f)所示就是原無向連通網的最小生成樹。
本 文以 無 向 網 的 鄰 接 矩 陣 類 NetAdjMatrix<T> 來 實 現 普 里 姆 算 法 。NetAdjMatrix<T>類的成員字段與無向圖鄰接矩陣類 GraphAdjMatrix<T>的成員字段一樣,不同的是,當兩個頂點間有邊相連接時, matirx 數組中相應元素的值是邊的權值,而不是 1。
無向網鄰接矩陣類 NetAdjMatrix<T>的實現如下所示。

1 /// <summary> 2 /// 無向網鄰接矩陣類 NetAdjMatrix<T>的實現如下所示。 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class NetAdjMatrix<T> : IGraph<T> 6 { 7 private Node<T>[] nodes;//頂點數組 8 private int numEdges;//邊的數目 9 private int[,] matrix;//鄰接矩陣數組 10 11 public NetAdjMatrix(int n) 12 { 13 nodes = new Node<T>[n]; 14 matrix = new int[n, n]; 15 numEdges = 0; 16 } 17 18 //獲取索引為index的頂點的信息 19 public Node<T> GetNode(int index) 20 { 21 return nodes[index]; 22 } 23 24 //設置索引為index的頂點的信息 25 public void SetNode(int index, Node<T> v) 26 { 27 nodes[index] = v; 28 } 29 30 //獲取matrix[index1, index2]的值 31 public int GetMatrix(int index1, int index2) 32 { 33 return matrix[index1, index2]; 34 } 35 36 //設置matrix[index1, index2]的值 37 public void SetMatrix(int index1, int index2, int v) 38 { 39 matrix[index1, index2] = v; 40 } 41 42 //邊的數目屬性 43 public int NumEdges 44 { 45 get { return numEdges; } 46 set { numEdges = value; } 47 } 48 49 //獲取頂點的數目 50 public int GetNumOfVertex() 51 { 52 return nodes.Length; 53 } 54 55 //獲取邊的數目 56 public int GetNumOfEdge() 57 { 58 return numEdges; 59 } 60 61 //判斷v是否是網的頂點 62 public bool IsNode(Node<T> v) 63 { 64 //遍歷頂點數組 65 foreach (Node<T> nd in nodes) 66 { 67 //如果頂點nd與v相等,則v是圖的頂點,返回true 68 if (v.Equals(nd)) 69 { 70 return true; 71 } 72 } 73 74 return false; 75 } 76 77 //獲取頂點v在頂點數組中的索引 78 public int GetIndex(Node<T> v) 79 { 80 int i = -1; 81 82 //遍歷頂點數組 83 for (i = 0; i < nodes.Length; ++i) 84 { 85 //如果頂點v與nodes[i]相等,則v是網的頂點,返回索引值i。 86 if (nodes[i].Equals(v)) 87 { 88 return i; 89 } 90 } 91 return i; 92 } 93 94 //在頂點v1和v2之間添加權值為v的邊 95 public void SetEdge(Node<T> v1, Node<T> v2, int v) 96 { 97 if (!IsNode(v1) || !IsNode(v2)) 98 { 99 Console.WriteLine("Node is not belong to Net!"); 100 return; 101 } 102 103 //矩陣是對稱矩陣 104 matrix[GetIndex(v1), GetIndex(v2)] = v; 105 matrix[GetIndex(v2), GetIndex(v1)] = v; 106 ++numEdges; 107 } 108 109 //刪除頂點v1和v2之間的邊 110 public void DelEdge(Node<T> v1, Node<T> v2) 111 { 112 //v1或v2不是無線網的頂點 113 if (!IsNode(v1) || !IsNode(v2)) 114 { 115 Console.WriteLine("Node is not belong to Net!"); 116 return; 117 } 118 119 //頂點v1與v2之間存在邊 120 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) 121 { 122 //矩陣是對稱矩陣 123 matrix[GetIndex(v1), GetIndex(v2)] = 0; 124 matrix[GetIndex(v2), GetIndex(v1)] = 0; 125 --numEdges; 126 } 127 } 128 129 //判斷頂點v1與v2之間是否存在邊 130 public bool IsEdge(Node<T> v1, Node<T> v2) 131 { 132 //v1或v2不是無向網的頂點 133 if (!IsNode(v1) || !IsNode(v2)) 134 { 135 Console.WriteLine("Node is not belong to Net!"); 136 return false; 137 } 138 139 //頂點v1與v2之間存在邊 140 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) 141 { 142 return true; 143 } 144 else //不存在邊 145 { 146 return false; 147 } 148 } 149 }
為實現普里姆算法,需要設置兩個輔助一維數組 lowcost 和 closevex, lowcost用來保存集合 V-U 中各頂點與集合 U 中各頂點構成的邊中具有最小權值的邊的權值; closevex 用來保存依附於該邊的在集合 U 中的頂點。假設初始狀態時,U={u1}(u1 為出發的頂點),這時有 lowcost[0]=0,它表示頂點 u1 已加入集合 U中。數組 lowcost 元素的值是頂點 u1 到其他頂點所構成的直接邊的權值。然后不斷選取權值最小的邊(ui,uk)(ui∈U,uk∈V-U),每選取一條邊,就將 lowcost[k]置為 0,表示頂點 uk 已加入集合 U 中。由於頂點 uk 從集合 V-U 進入集合 U 后,這兩個集合的內容發生了變化,就需要依據具體情況更新數組 lowcost 和 closevex中部分元素的值。把普里姆算法 Prim 作為 NetAdjMatrix<T>類的成員方法。
普里姆算法 Prim 的實現如下:

1 public int[] Prim() 2 { 3 int[] lowcost = new int[nodes.Length];//權值數組 4 int[] closevex = new int[nodes.Length];//頂點數組 5 int mincost = int.MaxValue;//最小權值 6 7 //輔助數組初始化 8 for (int i = 1; i < nodes.Length; ++i) 9 { 10 lowcost[i] = matrix[0, i]; 11 closevex[i] = 0; 12 } 13 14 //某個頂點加入集合U 15 lowcost[0] = 0; 16 closevex[0] = 0; 17 18 for (int i = 0; i < nodes.Length; ++i) 19 { 20 int k = 1; 21 int j = 1; 22 //選取權值最小的邊和相應的頂點 23 while (j < nodes.Length) 24 { 25 if (lowcost[j] < mincost && lowcost[j] != 0) 26 { 27 k = j; 28 } 29 ++j; 30 } 31 //新頂點加入集合U 32 lowcost[k] = 0; 33 //重新計算該頂點到其余頂點的邊的權值 34 for (j = 1; j < nodes.Length; ++j) 35 { 36 if (matrix[k, j] < lowcost[j]) 37 { 38 lowcost[j] = matrix[k, j]; 39 closevex[j] = k; 40 } 41 } 42 } 43 44 return closevex; 45 }
下表給出了在用普里姆算法構造圖 (a)的最小生成樹的過程中數組closevex 和 lowcost 及集合 U, V-U 的變化情況,讀者可進一步加深對普里姆算法的理解。
在普里姆算法中,第一個for循環的執行次數為n-1,第二個for循環中又包括了一個while循環和一個for循環,執行次數為 2(n-1)2,所以普里姆算法的時間復雜度為O(n2)。
下表在用普里姆算法構造圖 (a)的最小生成樹的過程中參數變化
3、 克魯斯卡爾(Kruskal)算法
克魯斯卡爾算法的基本思想是:對一個有 n 個頂點的無向連通網,將圖中的邊按權值大小依次選取,若選取的邊使生成樹不形成回路,則把它加入到樹中;若形成回路,則將它舍棄。如此進行下去,直到樹中包含有 n-1 條邊為止。
以(a)為例說明用克魯斯卡爾算法求無向連通網最小生成樹的過程。
第一步:首先比較網中所有邊的權值,找到最小的權值的邊(D,E),加入到生成樹的邊集 TE 中, TE={(D,E)}。
第二步:再比較圖中除邊(D,E)的邊的權值,又找到最小權值的邊(A,D)並且不會形成回路,加入到生成樹的邊集 TE 中, TE={(A,D),(D,E)}。
第三步:再比較圖中除 TE 以外的所有邊的權值,找到最小的權值的邊(A,B)並且不會形成回路,加入到生成樹的邊集 TE 中, TE={(A,D),(D,E),(A,B)}。
第四步:再比較圖中除 TE 以外的所有邊的權值,找到最小的權值的邊(E,C)並且不會形成回路,加入到生成樹的邊集 TE 中, TE={(A,D),(D,E),(A,B),(E,C)}。
此時,邊集 TE 中已經有 n-1 條邊,所以求圖 (a)的無向連通網的最小生成樹的過程已經完成,如下圖所示。這個結果與用普里姆算法得到的結果相同。
1.4.2:最短路徑
1、 最短路徑的概念
最短路徑問題是圖的又一個比較典型的應用問題。例如, n 個城市之間的一個公路網,給定這些城市之間的公路的距離,能否找到城市 A 到城市 B 之間一條距離最近的通路呢?如果城市用頂點表示,城市間的公路用邊表示,公路的長度作為邊的權值。那么,這個問題就可歸結為在網中求頂點 A 到頂點 B 的所有路徑中邊的權值之和最小的那一條路徑,這條路徑就是兩個頂點之間的最短路徑(Shortest Path),並稱路徑上的第一個頂點為源點(Source),最后一個頂點為終點(Destination)。在不帶權的圖中,最短路徑是指兩個頂點之間經歷的邊數最少的路徑。
網分為無向網和有向網,當把無向網中的每一條邊(vi,vj)都定義為弧<vi,vj>和弧<vj,vi>,則有向網就變成了無向網。因此,不失一般性,我們這里只討論有向網上的最短路徑問題。
下圖 是一個有向網及其鄰接矩陣。該網從頂點 A 到頂點 D 有 4 條路徑,分別是:路徑(A,D),其帶權路徑長度為 30;路徑(A,C,F,D),其帶權路徑長度為 22;路徑(A,C,B,E,D),其帶權路徑長度為 32;路徑(A,C,F,E,D),其帶權路徑長度為 34。路徑(A,C,F,D)稱為最短路徑,其帶權路徑長度 22 稱為最短距離。
2、 狄克斯特拉(Dikastra)算法
對於求單源點的最短路徑問題,狄克斯特拉(Dikastra)提出了一個按路徑長度遞增的順序逐步產生最短路徑的構造算法。狄克斯特拉的算法思想是:設置兩個頂點的集合 S 和 T,集合 S 中存放已找到最短路徑的頂點,集合 T 中存放當前還未找到最短路徑的頂點。初始狀態時,集合 S 中只包含源點,設為 v0,然后從集合 T 中選擇到源點 v0 路徑長度最短的頂點 u 加入到集合 S 中,集合 S 中每加入一個新的頂點 u 都要修改源點 v0 到集合 T 中剩余頂點的當前最短路徑長度值,集合 T 中各頂點的新的最短路徑長度值為原來的當前最短路徑長度值與從源點過頂點 u 到達該頂點的新的最短路徑長度中的較小者。此過程不斷重復,直到集合 T 中的頂點全部加到集合 S 中為止。
以上圖 為例說明用狄克斯特拉算法求有向網的從某個頂點到其余頂點最短路徑的過程。
下圖(a)~(f)給出了狄克斯特拉算法求從頂點 A 到其余頂點的最短路徑的過程。圖中虛線表示當前可選擇的邊,實線表示算法已確定包括到集合 S 中所有頂點所對應的邊。
第一步:列出頂點 A 到其余各頂點的路徑長度,它們分別為 0、∞、 5、 30、∞、∞。從中選取路徑長度最小的頂點 C(從源點到頂點 C 的最短路徑為 5)。
第二步:找到頂點 C 后,再觀察從源點經頂點 C 到各個頂點的路徑是否比第一步所找到的路徑要小(已選取的頂點則不必考慮),可發現,源點到頂點 B的路徑長度更新為 20(A, C, B),源點到頂點 F 的路徑長度更新為 12(A, C,F),其余的路徑則不變。然后, 從已更新的路徑中找出路徑長度最小的頂點 F(從源點到頂點 F 的最短路徑為 12)。
第三步:找到頂點 C、 F 以后,再觀察從源點經頂點 C、 F 到各頂點的路徑是否比第二步所找到的路徑要小(已被選取的頂點不必考慮),可發現,源點到頂點 D 的路徑長度更新為 22(A, C, F, D),源點到頂點 E 的路徑長度更新為30(A, C, F, E),其余的路徑不變。然后,從已更新的路徑中找出路徑長短最小的頂點 D(從源點到頂點 D 的最短路徑為 22)。
第四步:找到頂點 C、 F、 D 后,現在只剩下最后一個頂點 E 沒有找到最短路徑了,再觀察從源點經頂點 C、 F、 D 到頂點 E 的路徑是否比第三步所找到的路徑要小(已選取的頂點則不必考慮),可以發現,源點到頂點 E 的路徑長度更新為 28(A, C,B, E),其余的路徑則不變。然后,從已更新的路徑中找出路徑長度最小的頂點 E(從源點到頂點 E 的最短路徑為 28)。
3、 有向網的鄰接矩陣類的實現
本文以有向網的鄰接矩陣類 DirecNetAdjMatrix<T>來實現狄克斯特拉算法。DirecNetAdjMatrix<T>有三個成員字段,一個是 Node<T>類型的一維數組 nodes,存放有向網中的頂點信息,一個是整型的二維數組 matirx,表示有向網的鄰接矩陣,存放弧的信息,一個是整數 numArcs,表示有向網中弧的數目,有向網的鄰接矩陣類 DirecNetAdjMatrix<T>的實現如下所示。

1 /// <summary> 2 /// 有向網的鄰接矩陣類 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public class DirecNetAdjMatrix<T> : IGraph<T> 6 { 7 private Node<T>[] nodes; //有向網的頂點數組 8 private int numArcs; //弧的數目 9 private int[,] matrix; //鄰接矩陣數組 10 11 public DirecNetAdjMatrix(int n) 12 { 13 nodes = new Node<T>[n]; 14 matrix = new int[n, n]; 15 numArcs = 0; 16 } 17 18 //獲取索引為index的頂點的信息 19 public Node<T> GetNode(int index) 20 { 21 return nodes[index]; 22 } 23 24 //設置索引為index的頂點的信息 25 public void SetNode(int index, Node<T> v) 26 { 27 nodes[index] = v; 28 } 29 30 //獲取matrix[index1, index2]的值 31 public int GetMatrix(int index1, int index2) 32 { 33 return matrix[index1, index2]; 34 } 35 36 //設置matrix[index1, index2]的值 37 public void SetMatrix(int index1, int index2, int v) 38 { 39 matrix[index1, index2] = v; 40 } 41 42 //弧的數目屬性 43 public int NumArcs 44 { 45 get { return numArcs; } 46 set { numArcs = value; } 47 } 48 49 //獲取頂點的數目 50 public int GetNumOfVertex() 51 { 52 return nodes.Length; 53 } 54 55 //獲取弧的數目 56 public int GetNumOfEdge() 57 { 58 return numArcs; 59 } 60 61 //判斷v是否是網的頂點 62 public bool IsNode(Node<T> v) 63 { 64 //遍歷頂點數組 65 foreach (Node<T> nd in nodes) 66 { 67 //如果頂點nd與v相等,則v是網的頂點,返回true 68 if (v.Equals(nd)) 69 { 70 return true; 71 } 72 } 73 74 return false; 75 } 76 77 //獲取頂點v在頂點數組中的索引 78 public int GetIndex(Node<T> v) 79 { 80 int i = -1; 81 82 //遍歷頂點數組 83 for (i = 0; i < nodes.Length; ++i) 84 { 85 //如果頂點v與nodes[i]相等,則v是網的頂點,返回索引值i。 86 if (nodes[i].Equals(v)) 87 { 88 return i; 89 } 90 } 91 return i; 92 } 93 94 //在頂點v1和v2之間添加權值為v的弧 95 public void SetEdge(Node<T> v1, Node<T> v2, int v) 96 { 97 if (!IsNode(v1) || !IsNode(v2)) 98 { 99 Console.WriteLine("Node is not belong to Net!"); 100 return; 101 } 102 103 //有向 104 matrix[GetIndex(v1), GetIndex(v2)] = v; 105 ++numArcs; 106 } 107 108 //刪除頂點v1和v2之間的弧 109 public void DelEdge(Node<T> v1, Node<T> v2) 110 { 111 //v1或v2不是無線網的頂點 112 if (!IsNode(v1) || !IsNode(v2)) 113 { 114 Console.WriteLine("Node is not belong to Net!"); 115 return; 116 } 117 118 //有向 119 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) 120 { 121 matrix[GetIndex(v1), GetIndex(v2)] = int.MaxValue; 122 --numArcs; 123 } 124 } 125 126 //判斷頂點v1與v2之間是否存在弧 127 public bool IsEdge(Node<T> v1, Node<T> v2) 128 { 129 //v1或v2不是無向網的頂點 130 if (!IsNode(v1) || !IsNode(v2)) 131 { 132 Console.WriteLine("Node is not belong to Net!"); 133 return false; 134 } 135 136 //頂點v1與v2之間存在弧 137 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) 138 { 139 return true; 140 } 141 else //不存在弧 142 { 143 return false; 144 } 145 } 146 }
4、 狄克斯特拉算法的實現
為實現狄克斯特拉算法,引入兩個數組,一個一維數組 ShortPathArr,用來保存從源點到各個頂點的最短路徑的長度,一個二維數組 PathMatrixArr,用來保存從源點到某個頂點的最短路徑上的頂點,如 PathMatrix[v][w]為 true,則 w為從源點到頂點 v 的最短路徑上的頂點。為了該算法的結果被其他算法使用,把這兩個數組作為算法的參數使用。另外,為了表示某頂點的最短路徑是否已經找到,在算法中設了一個一維數組 final,如果 final[i]為 true,則表示已經找到第 i 個頂點的最短路徑。 i 是該頂點在鄰接矩陣中的序號。同樣,把該算法作為類DirecNetAdjMatrix<T>的成員方法來實現。

1 /// <summary> 2 /// 狄克斯特拉算法 3 /// </summary> 4 /// <param name="pathMatricArr">保存從源點到某個頂點的最短路徑上的頂點,如 PathMatrix[v][w]為 true,則 w為從源點到頂點 v 的最短路徑上的頂點</param> 5 /// <param name="shortPathArr"保存從源點到各個頂點的最短路徑的長度></param> 6 /// <param name="n">源點</param> 7 public void Dijkstra(ref bool[,] pathMatricArr, ref int[] shortPathArr, Node<T> n) 8 { 9 int k = 0; 10 bool[] final = new bool[nodes.Length]; 11 12 //初始化 13 for (int i = 0; i < nodes.Length; ++i) 14 { 15 final[i] = false; 16 shortPathArr[i] = matrix[GetIndex(n), i]; 17 18 for (int j = 0; j < nodes.Length; ++j) 19 { 20 pathMatricArr[i, j] = false; 21 } 22 23 if (shortPathArr[i] != 0 && shortPathArr[i] < int.MaxValue) 24 { 25 pathMatricArr[i, GetIndex(n)] = true; 26 pathMatricArr[i, i] = true; 27 28 } 29 } 30 31 // n為源點 32 shortPathArr[GetIndex(n)] = 0; 33 final[GetIndex(n)] = true; 34 35 //處理從源點到其余頂點的最短路徑 36 for (int i = 0; i < nodes.Length; ++i) 37 { 38 int min = int.MaxValue; 39 40 //比較從源點到其余頂點的路徑長度 41 for (int j = 0; j < nodes.Length; ++j) 42 { 43 //從源點到j頂點的最短路徑還沒有找到 44 if (!final[j]) 45 { 46 //從源點到j頂點的路徑長度最小 47 if (shortPathArr[j] < min) 48 { 49 k = j; 50 min = shortPathArr[j]; 51 } 52 } 53 } 54 55 //源點到頂點k的路徑長度最小 56 final[k] = true; 57 58 //更新當前最短路徑及距離 59 for (int j = 0; j < nodes.Length; ++j) 60 { 61 if (!final[j] && (min + matrix[k, j] < shortPathArr[j])) 62 { 63 shortPathArr[j] = min + matrix[k, j]; 64 for (int w = 0; w < nodes.Length; ++w) 65 { 66 pathMatricArr[j, w] = pathMatricArr[k, w]; 67 } 68 pathMatricArr[j, j] = true; 69 } 70 } 71 } 72 }
1.4.3:拓撲排序
拓撲排序(Topological Sort)是圖中重要的運算之一,在實際中應用很廣泛。例如,很多工程都可分為若干個具有獨立性的子工程,我們把這些子工程稱為“活動”。每個活動之間有時存在一定的先決條件關系,即在時間上有着一定的相互制約的關系。也就是說,有些活動必須在其它活動完成之后才能開始,即某項活動的開始必須以另一項活動的完成為前提。在有向圖中,若以圖中的頂點表示活動,以弧表示活動之間的優先關系,這樣的有向圖稱為 AOV 網(Active On VertexNetwork)。
在 AOV 網中,若從頂點 vi 到頂點 vj 之間存在一條有向路徑,則稱 vi 是 vj的前驅, vj 是 vi 的后繼。若<vi,vj>是 AOV 網中的弧,則稱 vi 是 vj 的直接前驅,vj 是 vi 的直接后繼。
例如,一個軟件專業的學生必須學習一系列的基本課程(如下表所示)。其中,有些課程是基礎課,如“高等數學”、“程序設計基礎”,這些課程不需要先修課程,而另一些課程必須在先學完某些課程之后才能開始學習。如通常在學完“程序設計基礎”和“離散數學”之后才開始學習“數據結構”等等。因此,可以用 AOV 網來表示各課程及其之間的關系。
在 AOV 網中,不應該出現有向環路,因為有環意味着某項活動以自己作為先決條件,這樣就進入了死循環。如果圖 6.19 的有向圖出現了有向環路,則教學計划將無法編排。因此,對給定的 AOV 網應首先判定網中是否存在環。檢測的辦法是對有向圖進行拓撲排序(Topological Sort),若網中所有頂點都在它的拓撲有序序列中,則 AOV 網中必定不存在環。
實現一個有向圖的拓撲有序序列的過程稱為拓撲排序。可以證明,任何一個有向無環圖,其全部頂點都可以排成一個拓撲序列,而其拓撲有序序列不一定是唯一的。例如,上圖的有向圖的拓撲有序序列有多個,這里列舉兩個如下:
(c1,c2,c3,c4,c5,c7,c8,c9,c10,c11,c6,c12,c8)和 (c9,c10,c11,c6,c1,c12,c4,c2,c3,c5,c7,c8)
由上面兩個序列可知,對於圖中沒有弧相連的兩個頂點,它們在拓撲排序的序列中出現的次序沒有要求。例如,第一個序列中 c1 先於 c9,第二個則反之。拓撲排序的任何一個序列都是一個可行的活動執行順序,它可以檢測到圖中是否存在環,因為如果有環,則環中的頂點無法輸出,所以得到的拓撲有序序列沒有包含圖中所有的頂點。
下面是拓撲排序算法的描述:
(1)在有向圖中選擇一個入度為 0 的頂點(即沒有前驅的頂點),由於該頂點沒有任何先決條件,輸出該頂點;
(2)從圖中刪除所有以它為尾的弧;
(3)重復執行(1)和(2),直到找不到入度為 0 的頂點,拓撲排序完成。如果圖中仍有頂點存在,卻沒有入度為 0 的頂點,說明 AOV 網中有環路,否則沒有環路。
如果圖中仍有頂點存在,卻沒有入度為 0 的頂點,說明 AOV 網中有環路,否則沒有環路。
以下圖 (a)為例求出它的一個拓撲有序序列。
第一步:在圖 (a)所示的有向圖中選取入度為 0 的頂點 c4,刪除頂點 c4及與它相關聯的弧<c4,c3>, <c4,c5>,得到圖 (b)所示的結果,並得到第一個拓撲有序序列頂點 c4。
第二步:再在圖 (b)中選取入度為 0 的頂點 c5,刪除頂點 c5 及與它相關聯的弧<c5,c6>,得到圖 (c)所示的結果,並得到兩個拓撲有序序列頂點 c4,c5。
第三步:再在圖 (c)中選取入度為 0 的頂點 c1,刪除頂點 c1 及與它相關聯的弧<c1,c2>, <c1,c3>,得到圖 (d)所示的結果,並得到三個拓撲有序序列頂點 c4, c5, c1。
第四步:再在圖 (d)中選取入度為 0 的頂點 c2,刪除頂點 c2 及與它相關聯的弧<c2,c6>,得到圖 (e)所示的結果,並得到四個拓撲有序序列頂點 c4,c5, c1, c2。
第五步:再在圖 (e)中選取入度為 0 的頂點 c3,刪除頂點 c3 及與它相關聯的弧<c3,c6>,得到圖 (f)所示的結果,並得到五個拓撲有序序列頂點 c4,c5, c1, c2, c3。
第六步:最后選取僅剩下的最后一個頂點 c6,拓撲排序結束,得到圖 (a)的一個拓撲有序序列(c4, c5, c1, c2, c3, c6)。
1.5:本章小結
圖是另一種比樹形結構更復雜的非線性數據結構,圖中的數據元素稱為頂點,頂點之間是多對多的關系。圖分為有向圖和無向圖,帶權值的圖稱為網。
圖的存儲結構很多,一般采用數組存儲圖中頂點的信息,鄰接矩陣采用矩陣也就是二維數組存儲頂點之間的關系。無向圖的鄰接矩陣是對稱的,所以在存儲時可以只存儲上三角矩陣或下三角矩陣的數據;有向圖的鄰接矩陣不是對稱的。鄰接表用一個鏈表來存儲頂點之間的關系,所以鄰接表是順序存儲與鏈式存儲相結合的存儲結構。
圖的遍歷方法有兩種:深度優先遍歷和廣度優先遍歷。圖的深度優先遍歷類似於樹的先序遍歷,是樹的先序遍歷的推廣,它訪問頂點的順序是后進先出,與棧一樣。圖的廣度優先遍歷類似於樹的層序遍歷,它訪問頂點的順序是先進先出,與隊列一樣。
圖的應用很廣,本章重點介紹了三個方面的應用。最小生成樹是一個無向連通網中邊的權值總和最小的生成樹。構造最小生成樹必須包括 n 個頂點、 n-1 條邊及不存在回路。構造最小生成樹的常用算法有普里姆算法和克魯斯卡爾算法兩種。
最短路徑問題是圖的一個比較典型的應用問題。最短路徑是網中求一個頂點到另一個頂點的所有路徑中邊的權值之和最小的路徑。可以求從一個頂點到網中其余頂點的最短路徑,這稱之為單源點問題,也可以求網中任意兩個頂點之間的最短路徑。本章只討論了單源點問題。解決單源點問題的算法是狄克斯特拉算法。
拓撲排序是圖中重要的運算之一,在實際中應用很廣泛。 AOV 網是頂點之間存在優先關系的有向圖。拓撲排序是解決 AOV 網中是否存在環路的有效手段,若拓撲有序序列中包含 AOV 網中所有的頂點,則 AOV 網中不存在環路,否則存在環路。