以下內容主要來自大話數據結構之中,部分內容參考互聯網中其他前輩的博客,主要是在自己理解的基礎上進行記錄。
圖的定義
圖是由頂點的有窮非空集合和頂點之間邊的集合組成,通過表示為G(V,E),其中,G標示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
無邊圖:若頂點Vi到Vj之間的邊沒有方向,則稱這條邊為無項邊(Edge),用序偶對(Vi,Vj)標示。
有向圖:若從頂點Vi到Vj的邊是有方向的,則成這條邊為有向邊,也稱為弧(Arc)。用有序對(Vi,Vj)標示,Vi稱為弧尾,Vj稱為弧頭。如果任意兩條邊之間都是有向的,則稱該圖為有向圖。
有向圖G2中,G2=(V2,{E2}),頂點集合(A,B,C,D),弧集合E2={<A,D>,{B,A},<C,A>,<B,C>}.
權(Weight):有些圖的邊和弧有相關的數,這個數叫做權(Weight)。這些帶權的圖通常稱為網(Network)。
圖的存儲結構
圖的存儲結構一般分為鄰接矩陣和十字鏈表
鄰接矩陣:圖的鄰接矩陣存儲方式是用兩個數組來標示圖。一個一位數組存儲圖頂點的信息,一個二維數組(稱為鄰接矩陣)存儲圖中邊或者弧的信息。
設圖G有n個頂點,則鄰接矩陣是一個n*n的方陣,定義為:
十字鏈表:
頂點表結點結構:
firstin:表示入邊表頭指針,指向該頂點的入邊表中第一個結點。
firstout:表示出邊表頭指針,指向該頂點的出邊表中的第一個結點。
邊表結點結構:
tailvex:指弧起點在頂點表的下標。
headvex:指弧終點在頂點表中的下標。
headlink:指入邊表指針域。
taillink:指邊表指針域。
如果是網,還可以再增加一個weight域來存儲權值。
藍線表示出度,紅線表示入度
十字鏈表的優點:
十字鏈表是把鄰接表和逆鄰接表整合在一起,這樣既容易找到以Vi為尾的弧,也容易找到以Vi為頭的弧,
因而容易求的頂點的出度和入度。
圖的搜索:
深度優先遍歷:也有稱為深度優先搜索,簡稱DFS。其實,就像是一棵樹的前序遍歷。它從圖中某個結點v出發,訪問此頂點,然后從v的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有 路徑相通的頂點都被訪問到。若圖中尚有頂點未被訪問,則另選圖中一個未曾被訪問的頂點作起始點,重復上述過程,直至圖中的所有頂點都被訪問到為止。
基本實現思想:
(1)訪問頂點v;
(2)從v的未被訪問的鄰接點中選取一個頂點w,從w出發進行深度優先遍歷;
(3)重復上述兩步,直至圖中所有和v有路徑相通的頂點都被訪問到。
廣度優先遍歷:也稱廣度優先搜索,簡稱BFS。BFS算法是一個分層搜索的過程,和樹的層序遍歷算法類同,它也需要一個隊列以保持遍歷過的頂點順序,以便按出隊的順序再去訪問這些頂點的鄰接頂點。
基本實現思想:
(1)頂點v入隊列。
(2)當隊列非空時則繼續執行,否則算法結束。
(3)出隊列取得隊頭頂點v;訪問頂點v並標記頂點v已被訪問。
(4)查找頂點v的第一個鄰接頂點col。
(5)若v的鄰接頂點col未被訪問過的,則col入隊列。
(6)繼續查找頂點v的另一個新的鄰接頂點col,轉到步驟(5)。
直到頂點v的所有未被訪問過的鄰接點處理完。轉到步驟(2)。
廣度優先遍歷圖是以頂點v為起始點,由近至遠,依次訪問和v有路徑相通而且路徑長度為1,2,……的頂點。為了使“先被訪問頂點的鄰接點”先於“后被訪問頂點的鄰接點”被訪問,需設置隊列存儲訪問的頂點。
最小生成樹
我們把構造連通網的最小代價生成的樹稱為最小生成樹,即權值最小的生成樹。
實現方式:
1、普利姆算法(Prim)
基本思想:假設G=(V,E)是連通的,TE是G上最小生成樹中邊的集合。算法從U={u0}(u0∈V)、TE={}開始。重復執行下列操作:
在所有u∈U,v∈V-U的邊(u,v)∈E中找一條權值最小的邊(u0,v0)並入集合TE中,同時v0並入U,直到V=U為止。
此時,TE中必有n-1條邊,T=(V,TE)為G的最小生成樹。
Prim算法的核心:始終保持TE中的邊集構成一棵生成樹。
注意:prim算法適合稠密圖,其時間復雜度為O(n^2),其時間復雜度與邊得數目無關,而kruskal算法的時間復雜度為O(eloge)跟邊的數目有關,適合稀疏圖。
示例:
(1)圖中有6個頂點v1-v6,每條邊的邊權值都在圖上;在進行prim算法時,我先隨意選擇一個頂點作為起始點,當然我們一般選擇v1作為起始點,好,現在我們設U集合為當前所找到最小生成樹里面的 頂點,TE集合為所找到的邊,現在狀態如下:
U={v1}; TE={};
(2)現在查找一個頂點在U集合中,另一個頂點在V-U集合中的最小權值,如下圖,在紅線相交的線上找最小值。
通過圖中我們可以看到邊v1-v3的權值最小為1,那么將v3加入到U集合,(v1,v3)加入到TE,狀態如下:
U={v1,v3}; TE={(v1,v3)};
(3)繼續尋找,現在狀態為U={v1,v3}; TE={(v1,v3)};在與紅線相交的邊上查找最小值。
我們可以找到最小的權值為(v3,v6)=4,那么我們將v6加入到U集合,並將最小邊加入到TE集合,那么加入后狀態如下:
U={v1,v3,v6}; TE={(v1,v3),(v3,v6)}; 如此循環一下直到找到所有頂點為止。
(4)下圖像我們展示了全部的查找過程:
2、克魯斯卡爾算法(Kruskal)
假設連通網N=(V,{E})。則令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T=(V,{}),圖中每個頂點自成一個連通分量。在E中選擇最小代價的邊,若該邊依附的頂點落在T中不同的連通分 量中,則將該邊加入到T中,否則舍去此邊而選擇下一條代價最小的邊,依次類推,直到T中所有頂點都在同一連通分量上為止。
示例如下:
圖中先將每個頂點看作獨立的子圖,然后查找最小權值邊,這條邊是有限制條件的,邊得兩個頂點必須不在同一個圖中,如上圖,第一個圖中找到最小權值邊為(v1,v3),且滿足限制條件,繼續查找邊 (v4,v6),(v2,v5),(v3,v6),當查找到最后一條邊時,僅僅只有(v2,v3)滿足限制條件,其他的如(v3,v4),(v1,v4)都在一個子圖里面,不滿足條件,至此已經找到最小生成樹的
所有邊。
上述所有的具體代碼實現:
1 /** 2 * java數據結構無向圖的實現 3 * 2016/4/29 4 */ 5 package cn.Link; 6 7 import java.util.ArrayList; 8 import java.util.LinkedList; 9 import java.util.Queue; 10 public class Graph { 11 12 final static int MAX = 65535; //兩個定點之間沒有路徑時的長度 13 int verticts; //頂點數 14 int sides; //頂點為verticts的連通圖的邊數值 15 int[][] arc;//存儲圖的二維數組 16 String[] vex; 17 Graph(int verticts){ 18 this.verticts = verticts; 19 this.sides = 0; 20 for(int i = verticts-1;i>0;i--){ 21 this.sides +=i; //獲得連通圖的邊數值 22 } 23 this.arc = new int[verticts][verticts]; 24 this.vex = new String[verticts]; 25 //初始化一個有向圖,所有邊設為最大權值(即不可能達到的值) 26 for(int i = 0; i < verticts;i++){ 27 for(int j = 0; j < verticts;j++){ 28 if(i!=j){ 29 this.arc[i][j] = MAX; 30 }else{ 31 this.arc[i][j]=0; 32 } 33 34 } 35 } 36 } 37 38 //假設有這樣一個圖 39 public void addGraph(){ 40 //頂點數據 41 this.vex[0] = "beijing"; 42 this.vex[1] = "shanghai"; 43 this.vex[2] = "tianjing"; 44 this.vex[3] = "chengdu"; 45 this.vex[4] = "changsha"; 46 this.vex[5] = "chongqing"; 47 48 //邊的權值 49 for(int i = 1; i < verticts;i++){ 50 for(int j = 0; j < i;j++){ 51 int n = (int)(Math.random()*100); //隨機生成權值 52 if(n > 0){ 53 this.arc[i][j]=this.arc[j][i] = n; 54 }else if(n == 0){ 55 this.arc[i][j]=this.arc[j][i] = MAX; 56 } 57 58 } 59 } 60 } 61 62 //利用圖的二維數組輸出 63 public void printGraph(){ 64 for(int i = 0; i < verticts;i++){ 65 //輸出第一行的名稱 66 if(i == 0){ 67 System.out.print("* "); 68 for(int x=0;x<verticts;x++){ 69 System.out.print(this.vex[x]+" "); 70 } 71 System.out.println(); 72 System.out.println(" =============================================================="); 73 } 74 //給每行前面輸出地址 75 System.out.print(this.vex[i]+" "); 76 for(int j = 0; j < verticts;j++){ 77 System.out.print(this.arc[i][j]+" "); 78 } 79 System.out.println(); 80 System.out.println(); 81 } 82 } 83 84 //深度優先遍歷輸出 85 public void DFS(Graph G,int i,boolean[] visited){ 86 int j; 87 visited[i] = true; 88 System.out.print(G.vex[i]+" "); //打印頂點的值 89 for(j = 0;j < G.verticts;j++){ 90 if(G.arc[i][j]>0 && !visited[j]){ 91 DFS(G,j,visited); //對訪問的鄰接頂點遞歸調用 92 } 93 } 94 95 } 96 public void DFSTraverse(Graph G){ 97 boolean[] visited = new boolean[G.verticts]; 98 for(int i = 0;i < G.verticts;i++){ 99 visited[i] = false; //初始狀態所有頂點都是未訪問過的狀態 100 } 101 for(int i = 0;i < G.verticts;i++){ 102 if(!visited[i]) 103 DFS(G,i,visited); //對未訪問過的頂點調用DFS 如果是連通圖,則只會執行一次 104 } 105 } 106 107 108 109 //廣度優先遍歷輸出 110 public void BFS(Graph G){ 111 int i, j; 112 Queue<Integer> Q = new LinkedList<Integer>(); 113 boolean[] visited = new boolean[G.verticts]; 114 for(i = 0;i < G.verticts;i++){ 115 visited[i] = false; 116 } 117 118 for(i = 0; i < G.verticts;i++){ //對每一個頂點都進行循環 119 if(!visited[i]){ 120 visited[i] = true; 121 System.out.print("##"+G.vex[i]+" "); 122 Q.offer(i); 123 while(Q != null){ 124 if(Q.peek() != null){ 125 i = Q.poll(); //將隊首元素賦值給i 然后出隊列 126 }else{return ;} 127 for(j = 0;j < G.verticts;j++){ 128 //判斷其他頂點與當前頂點存在邊但未被訪問過 129 if(G.arc[i][j] > 0 && !visited[j]){ 130 visited[j] = true; 131 System.out.print("##"+G.vex[j]+" "); 132 Q.offer(j); 133 } 134 } 135 } 136 } 137 } 138 } 139 140 //得到最小生成樹之普利姆(Prim)算法 141 public void MiniSpanTree_Prim(Graph G){ 142 int min, i, j, k; 143 int [] adjvex = new int[G.verticts]; //保存相關頂點下表 144 int [] lowcost = new int[G.verticts]; //保存相關頂點間邊的權值 145 lowcost[0] = 0; //初始化第一個權值為0 ,即vex[0]已經加入到生成樹 146 adjvex[0] = 0; //初始化第一個頂點下標為0 147 for(i = 1;i < G.verticts;i++){ 148 lowcost[i] = G.arc[0][i]; //將vex[0]頂點與之有邊的權值存入數組 149 adjvex[i] = 0; //初始化都為vex[0]的下標 150 } 151 152 for(i = 1;i < G.verticts;i++){ 153 min = MAX; //初始化最小權值為MAX:65535 154 j = 1; 155 k = 0; 156 //循環所有頂點 157 while(j < G.verticts){ 158 if(lowcost[j] != 0 && lowcost[j] < min){ //如果權值不為0,而且小於最小值 159 min = lowcost[j]; 160 k = j; 161 } 162 j++; 163 } 164 165 System.out.println("("+k+", "+adjvex[k]+")"+" 權長:"+G.arc[adjvex[k]][k]); //打印當前頂點邊中權值最小的邊 166 lowcost[k] = 0; //將當前頂點的權值設為0,表示此頂點已將完成任務 167 for(j = 1;j < G.verticts;j++){ //循環所有頂點 168 if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){ //如果下標為k的頂點各邊權值小於此前這些頂點未被加入生成權值 169 lowcost[j] = G.arc[k][j]; //將較小的權值存入lowcost 170 adjvex[j] = k; //將下標為k的頂點存入adjvex 171 } 172 } 173 } 174 } 175 176 177 //得到最小生成樹之克魯斯卡爾(Kruskal)算法 178 //得到邊集數組並按權由大到小排序 這一步是利用Edge類來實現的 179 //注意 此函書還有錯誤,有時候會輸出6條邊,尚待解決(MinispanTree_kruskal在尋找的過程中不可能形成環路,所以不可能多一條邊) 180 //上述錯誤已經解決,有兩個地方出來問題,第一:Edge的begin必須小於end,否則在Find函數中判斷將出現錯誤,因為如果end小於begin的話,end有可能 181 //出現等於0的情況,第二:循環每一條邊時,i應該小於G.sides;而我之前寫成了i<G.verticts 182 public void MiniSpanTree_Kruskal(Graph G){ 183 Edge edge = new Edge(); 184 edge.Edge_1(G); 185 //edge.PrintEdge(); 186 int i, n, m; 187 int num = 0; //記錄找到了多少條邊 188 int parent[] = new int[G.verticts]; //定義一個數組用來判斷邊與邊是否形成回路 189 for( i = 0;i < G.verticts;i++){ 190 parent[i] = 0; //初始化數組為-1 191 } 192 for(i = 0;i < G.sides;i++){ // 循環每一條邊,15為頂點 193 n = Find(parent,edge.edge[i].begin); 194 m = Find(parent,edge.edge[i].end); 195 if(n != m ){ //n不等於m 說明邊與邊沒有形成環路 196 parent[n] = m;//將此邊的尾節點放入下標為起點的parent中 197 System.out.println("("+edge.edge[i].begin+","+edge.edge[i].end+")"+" 權長:"+edge.edge[i].weight); 198 num++; 199 //for(int j = 0;j < G.verticts;j++){ 200 //System.out.print(" !!!!"+parent[j]); //初始化數組為0 201 //} 202 } 203 if(num >= G.verticts) break; //如果找到了(頂點數-1)條邊,並且沒有構成回路,就已經完成任務了,不用再找了, 204 205 } 206 } 207 public int Find(int[] parent,int f){ //查找連線頂點的尾部下表 208 while(parent[f] > 0){ 209 f = parent[f]; 210 } 211 return f; 212 } 213 214 215 //測試函數 216 public static void main(String[] args){ 217 Graph graph = new Graph(6); //創建一個頂點個數為6的圖 218 graph.addGraph(); 219 System.out.println("將圖以二維矩陣的方式輸出"); 220 graph.printGraph(); 221 System.out.println("深度優先搜索結果"); 222 graph.DFSTraverse(graph); 223 System.out.println(); 224 System.out.print("廣度優先搜索結果"); 225 System.out.println(); 226 graph.BFS(graph); 227 System.out.println(); 228 System.out.println("最小生成樹之普利姆算法Prim "); 229 graph.MiniSpanTree_Prim(graph); 230 System.out.println(); 231 System.out.println("最小生成樹之克魯斯卡爾算法Kruskal "); 232 graph.MiniSpanTree_Kruskal(graph); 233 234 } 235 } 236 237 238 239 240 //Edge類 利用深度優先遍歷得到樹的所有路徑以及這些路徑的權值,並根據權值的大小進行從小到大排序 241 class Edge{ 242 public int begin; //這兩個頂點的開始頂點 243 public int end; //這兩個頂點的結束頂點 244 public int weight; //兩個頂點之間的權值 245 Edge edge[] = new Edge[15]; //edge數組 圖的邊數沒有傳入,計算最大值 246 Edge(){} 247 public void Edge_1(Graph G){ 248 DFSTraverse_1(G); //得到edge數組 249 sortEdge(); //對dege進行排序 250 } 251 252 public void SetEdge(int begin,int end,int weight){ 253 this.begin = begin; 254 this.end = end; 255 this.weight = weight; 256 257 } 258 int k=0; //用於數組賦值是計數 259 //利用深度優先遍歷得到edge數組 260 public void DFS_1(Graph G,int i,boolean[] visited){ 261 int j; 262 263 visited[i] = true; 264 //System.out.print(G.arc[i][i+1]+" "); //打印頂點的值 265 for(j = 0;j < G.verticts;j++){ 266 if(G.arc[i][j]>0 && !visited[j]){ 267 //System.out.print(G.arc[i][j]+" "); 268 DFS_1(G,j,visited); //對訪問的鄰接頂點遞歸調用 269 } 270 if(G.arc[i][j] > 0 && i > j){ 271 this.edge[this.k] = new Edge(); 272 edge[this.k].SetEdge(j,i,G.arc[i][j]); 273 this.k++; 274 } 275 276 } 277 278 } 279 public void DFSTraverse_1(Graph G){ 280 boolean[] visited = new boolean[G.verticts]; 281 for(int i = 0;i < G.verticts;i++){ 282 visited[i] = false; //初始狀態所有頂點都是未訪問過的狀態 283 } 284 for(int i = 0;i < G.verticts;i++){ 285 if(!visited[i]) 286 DFS_1(G,i,visited); //對未訪問過的頂點調用DFS 如果是連通圖,則只會執行一次 287 } 288 } 289 //對得到的數組進行排序 290 public void sortEdge(){ 291 Edge newEdge = new Edge(); 292 newEdge.edge[0] = new Edge(); 293 for(int i = 0;i < this.edge.length;i++){ 294 for(int j = i;j < this.edge.length;j++){ 295 if(this.edge[i].weight > this.edge[j].weight){ 296 newEdge.edge[0] = this.edge[i]; 297 this.edge[i] = this.edge[j]; 298 this.edge[j] = newEdge.edge[0]; 299 } 300 } 301 } 302 } 303 //輸出Edge數組,用以測試Edge是否創建、賦值成功 304 public void PrintEdge(){ 305 for(int i = 0; i < this.edge.length;i++){ 306 System.out.println("數組"+i+": "+this.edge[i].begin+" "+this.edge[i].end+" "+this.edge[i].weight); 307 } 308 } 309 }