---恢復內容開始---
作者版權所有,轉載請注明出處,多謝. http://www.cnblogs.com/Henvealf/p/5534071.html
前些天因為某些原因,就試着回想一下圖(graph)的相關內容,沒想腦子里一片空白,只怪當初沒有好好聽這門課.然后就學習了一下,這里做個小總結.
1.概念
簡單圖(simple graph):就是由一些頂點(V,vertice) 和 連接這些頂點的一些邊(E,edge)所組成的結構,並且每對頂點之間只能存在一條邊.所以通常會用G = (V,E)來表示一個簡單圖.簡單圖也被稱為無向圖(undirected graph).
說到無向圖就一定有有向圖(directed graph),有向圖同樣也用G = (V,E)來表示,都明白兩種圖的區別是什么,就不多說了.不過在這里說一些表示邊時候的區別:
簡單圖的邊使用{Vi,Vj}的方式表示,表示連接頂點i與頂點j的邊.因為是簡單圖,所以有:
- {Vi,Vj} = {Vj,Vi,}
有向圖的邊使用(Vi,Vj)的方式表示.意思是當前邊的方向是從頂點i到頂點j,所以很明顯有:
- (Vi,Vj) ≠ (Vj,Vi)
2.圖的表示
有很多方法可以表示一個圖,不過思想上大致都一樣.比如下面一個圖(本文之后都會用這個圖來作為例子):
圖1
自己畫的,或許丑了點:)
先看使用鄰接表(adjacency list)來表示該圖:
V0 | V1 V2 |
V1 | V0 V3 |
V2 | V0 V3 V4 |
V3 | V1 V2 V5 V6 |
V4 | V2 V7 |
V5 | V3 V6 |
V6 | V3 V5 |
V7 | V4 |
仔細觀察就會發現,鄰接表是在首列按順序(正序倒序都可)列出所有頂點,然后第二行列出與頂點相鄰的所有頂點,例如第一行,圖中與V0相鄰的頂點有V1與V2.所以第二列的內容便是 V1 與 V2.
另一種表示方法是使用鄰接矩陣(adjacency matrix),思想與鄰接表大致相同,不同的是鄰接表是將與某一頂點相鄰的頂點們列出,而鄰接矩陣是將他們標注出。顧名思義就是使用矩陣表示,如在本例中,一共有8個頂點,所以此時鄰接矩陣的大小就為8*8。如下:
V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | |
V0 | oo | 1 | 1 | oo | oo | oo | oo | oo |
V1 | oo | oo | oo | 1 | oo | oo | oo | oo |
V2 | oo | oo | oo | 1 | 1 | oo | oo | oo |
V3 | oo | 1 | 1 | oo | oo | 1 | 1 | oo |
V4 | oo | oo | 1 | oo | oo | oo | oo | 1 |
V5 | oo | oo | oo | 1 | oo | oo | 1 | oo |
V6 | oo | oo | oo | 1 | oo | 1 | oo | oo |
V7 | oo | oo | oo | oo | 1 | oo | oo | oo |
觀察可以發現,如果頂點i與頂點j之間存在邊,就將矩陣的aij項設為1,其他情況就設為無窮大(oo)。公式就是:
{ 1 邊(Vi ,Vj)存在
aij = {
{ 無窮大 其他情況
如果對於有向圖,此公式就得改為:
{ 1 邊{Vi ,Vj}存在
aij = {
{ 無窮大 其他情況
對於使用哪種表示法,這要取決與你所要處理的問題,如果你僅僅只是處理與某一頂點鄰接的頂點,例如遍歷,很明顯使用表比使用矩陣所要步數要少很多。當你需要對圖進行插入或者刪除頂點的操作,就應該使用鄰接矩陣,你只需要將矩陣上的數從0變為1,或者從1變為0即可。而使用鄰接表,你還得要對表進行維護。
本例就選用鄰接矩陣來表示圖1,用二維數組就能直接實現,無窮大被換成了名為oo的變量.如下:
1 int oo = Integer.MAX_VALUE; 2 int[][] racs1 = new int[][]{ 3 {oo, 1, 1,oo,oo,oo,oo,oo}, 4 { 1,oo,oo, 1,oo,oo,oo,oo}, 5 { 1,oo,oo, 1, 1,oo,oo,oo}, 6 {oo, 1, 1,oo,oo, 1, 1,oo}, 7 {oo,oo, 1,oo,oo,oo,oo, 1}, 8 {oo,oo,oo, 1,oo,oo, 1,oo}, 9 {oo,oo,oo, 1,oo, 1,oo,oo}, 10 {oo,oo,oo,oo, 1,oo,oo,oo}, 11 };
除此之外,這里還設定義了一個數組,用於為每個頂點添加一些信息.也就是各個頂點的名字.如果對與具體的問題,例如每個頂點代表地圖上的一個城市,可使用像"北京","上海"城市名.當然可以很據特定情況將頂點的相關內容封裝一下.
1 String[] verticeInfos1 = new String[] { 2 "V0","V1","V2","V3","V4","V5","V6","V7" 3 };
另:有一種圖中有孤立頂點的情況,假設上面的圖中有還有一個頂點V8,但它不與任何其他頂點相鄰,則該怎么用表或者矩陣來表示那?就當留給大家的一個小問題。
3.圖的遍歷
3.1 先寫一個名為Graph的類.用這個類來封裝圖的相關字段和遍歷方法.先看類的字段與構造方法:
1 /** 2 * 使用鄰接矩陣實現圖<p> 3 * 深度優先遍歷與廣度優先遍歷<p> 4 * 求最短路徑:<p> 5 * 1. Dijkstra 算法 <p> 6 * 2. Ford 算法 <p> 7 * 3. 通用型的糾正標記算法<p> 8 * Created by Henvealf on 16-5-22. 9 */ 10 public class Graph<T> { 11 private int[][] racs; //鄰接矩陣 12 private T[] verticeInfo; //各個點所攜帶的信息. 13 14 private int verticeNum; //節點的數目, 15 private int[] visitedCount; //記錄訪問 16 private int[] currDist; //最短路徑算法中用來記錄每個節點的當前路徑長度. 17 18 public Graph(int[][] racs, T[] verticeInfo){ 19 if(racs.length != racs[0].length){ 20 throw new IllegalArgumentException("racs is not a adjacency matrix!"); 21 } 22 if(racs.length != verticeInfo.length ){ 23 throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!"); 24 } 25 this.racs = racs; 26 this.verticeInfo = verticeInfo; 27 verticeNum = racs.length; 28 visitedCount = new int[verticeNum]; 29 } 30 //.....其他方法 31 }
這里使用了模板來模板化 verticeInfos.
還要說明的便是數組 int[] visitedCount; 其作用是為了標記圖中的頂點是否被訪問過. visitedCount[i] == 0 就說明頂點 i 還未被訪問過,所以在進行遍歷操作前需要初始化該數組全為0.方法如下:
1 /** 2 * 將記錄訪問的數組初始化為0 3 */ 4 private void initVisitedCount(){ 5 for(int i = 0; i < visitedCount.length; i ++){ 6 visitedCount[i] = 0; 7 } 8 }
3.2 圖的遍歷和樹的遍歷類似,分為深度優先遍歷與廣度優先遍歷.
深度優先遍歷,簡單說就是先沿着一條路線最靠左或最靠右的路線往下走,並把路過的頂點標記為已訪問.一直走到無路可走,或者下一站的頂點都被已經訪問過了,就順着剛才走過的路往回看(回溯),當發現回溯中的某一頂點還有其他未被訪問過的分支的時候,就選擇這個分支繼續,重復上面的過程繼續遍歷.直到回溯到了遍歷的出發點,就結束遍歷.但如果檢查發現圖中還存在未被訪問過的頂點,就說明圖中還存在孤立與本圖的分圖.此時則任意選擇一個未被訪問過的頂點繼續訪問. 最后當圖中所有的頂點都被訪問過的時候,就說明遍歷完成.
所以這里需要一個方法,用來判斷圖中頂點的訪問情況:
1 /** 2 * 尋找沒有被訪問過的頂點. 3 * @return > 0 即為還未被訪問過的頂點. -1 說明所有的節點都被訪問過了. 4 */ 5 private int findNotVisited(){ 6 for(int i = 0; i < noteNum; i ++){ 7 if(visitedCount[i] == 0){ 8 return i; 9 } 10 } 11 return -1; 12 }
要尋找與頂點的相鄰節點時,我們可以發現簡單圖的鄰接矩陣以對角線對稱,所以尋找相鄰頂點的時候只需要遍歷一半就可以,大大的提高了遍歷的效率,不過對於有向圖就需要遍歷全圖.這里只討論簡單圖,有向圖自行修改區分便可,這里是遍歷全圖:
1 /** 2 * 深度遍歷的遞歸 3 * @param begin 從第幾個節點開始遍歷 4 */ 5 public void DFS(int begin, Queue<T> edges){ 6 visitedCount[begin] = 1; //標記begin為已訪問 7 edges.offer(verticeInfo[begin]); //加入記錄隊列 8 for(int a = 0; a < verticeNum; a++){ //遍歷相鄰的點 9 if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){ //相鄰的點未被訪問過 10 DFS(a,edges); 11 } 12 } 13 } 14 15 /** 16 * 開始深度優先遍歷 17 * @return 返回保持有遍歷之后的順序的隊列 18 */ 19 public Queue<T> depthFirstSearch(){ 20 initVisitedCount(); //將記錄訪問次序的數組初始化為0 21 Queue<T> edges = new LinkedList<>(); //用於存儲遍歷過的點,用於輸出 22 int begin = -1; 23 while((begin = findNotVisited()) != -1){ //不等於-1說明還有未訪問過的點 24 DFS(begin,edges); 25 } 26 return edges; 27 }
廣度優先遍歷.與樹的廣度優先遍歷相似,就是逐層遍歷.代碼如下:
1 /** 2 * 廣度優先遍歷 3 * @return 返回保持有遍歷之后的順序的隊列 4 */ 5 public Queue<T> breadthFirstSearch(){ 6 initVisitedCount(); //將記錄訪問次序的數組初始化為0 7 Queue<Integer> tallyQueue = new LinkedList<>(); //初始化隊列 8 Queue<T> edges = new LinkedList<>(); //用於存儲遍歷過的點,用於輸出 9 int nowVertice = -1; //當前所在的點 10 while((nowVertice = findNotVisited()) != -1){ //尋找還未被訪過問的點 11 visitedCount[nowVertice] = 1; //設置訪問標記 12 edges.offer(verticeInfo[nowVertice]); 13 tallyQueue.offer(nowVertice); //將當前孤立部分一個頂點加入記錄隊列中 14 while(!tallyQueue.isEmpty()){ //只要隊列不為空 15 nowVertice = tallyQueue.poll(); //取出隊首的節點 16 for(int a = 0; a < verticeNum; a++){ //遍歷所有和nowVertice相鄰的節點 17 if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) { //沒有訪問過 18 visitedCount[a] = 1; //記為標記過 19 tallyQueue.offer(a); //加入隊列,上面會繼續取出.來遍歷 20 edges.offer(verticeInfo[a]); //記錄 21 } 22 } 23 } 24 } 25 return edges; 26 }
這里需要兩個隊列,一個隊列用於存儲遍歷過程,另一個用於保存第n層被遍歷的順序,等到遍歷第n+1層的時候,取出的頂點就是按照上層的順序來排列,也就能按照上層的順序來繼續遍歷.
下面是Graph完整代碼:
1 package com.henvealf.datastructures.graph.arcs; 2 3 import java.util.*; 4 5 /** 6 * 使用鄰接矩陣實現圖<p> 7 * 深度優先遍歷與廣度優先遍歷<p> 8 * 求最短路徑:<p> 9 * 1. Dijkstra 算法 <p> 10 * 2. Ford 算法 <p> 11 * 3. 通用型的糾正標記算法<p> 12 * Created by Henvealf on 16-5-22. 13 */ 14 public class Graph<T> { 15 private int[][] racs; //鄰接矩陣 16 private T[] verticeInfo; //各個點所攜帶的信息. 17 18 private int verticeNum; //節點的數目, 19 private int[] visitedCount; //記錄訪問 20 private int[] currDist; //最短路徑算法中用來記錄每個節點的當前路徑長度. 21 22 public Graph(int[][] racs, T[] verticeInfo){ 23 if(racs.length != racs[0].length){ 24 throw new IllegalArgumentException("racs is not a adjacency matrix!"); 25 } 26 if(racs.length != verticeInfo.length ){ 27 throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!"); 28 } 29 this.racs = racs; 30 this.verticeInfo = verticeInfo; 31 verticeNum = racs.length; 32 visitedCount = new int[verticeNum]; 33 } 34 35 /** 36 * 深度遍歷的遞歸 37 * @param begin 從第幾個節點開始遍歷 38 */ 39 public void DFS(int begin, Queue<T> edges){ 40 visitedCount[begin] = 1; //標記begin為已訪問 41 edges.offer(verticeInfo[begin]); //加入記錄隊列 42 for(int a = 0; a < verticeNum; a++){ //遍歷相鄰的點 43 if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){ //相鄰的點未被訪問過 44 DFS(a,edges); 45 } 46 } 47 } 48 49 /** 50 * 開始深度優先遍歷 51 * @return 返回保持有遍歷之后的順序的隊列 52 */ 53 public Queue<T> depthFirstSearch(){ 54 initVisitedCount(); //將記錄訪問次序的數組初始化為0 55 Queue<T> edges = new LinkedList<>(); //用於存儲遍歷過的點,用於輸出 56 int begin = -1; 57 while((begin = findNotVisited()) != -1){ //不等於-1說明還有未訪問過的點 58 DFS(begin,edges); 59 } 60 return edges; 61 } 62 63 /** 64 * 廣度優先遍歷 65 * @return 返回保持有遍歷之后的順序的隊列 66 */ 67 public Queue<T> breadthFirstSearch(){ 68 initVisitedCount(); //將記錄訪問次序的數組初始化為0 69 Queue<Integer> tallyQueue = new LinkedList<>(); //初始化隊列 70 Queue<T> edges = new LinkedList<>(); //用於存儲遍歷過的點,用於輸出 71 int nowVertice = -1; //當前所在的點 72 while((nowVertice = findNotVisited()) != -1){ //尋找還未被訪過問的點 73 visitedCount[nowVertice] = 1; //設置訪問標記 74 edges.offer(verticeInfo[nowVertice]); 75 tallyQueue.offer(nowVertice); //將當前孤立部分一個頂點加入記錄隊列中 76 while(!tallyQueue.isEmpty()){ //只要隊列不為空 77 nowVertice = tallyQueue.poll(); //取出隊首的節點 78 for(int a = 0; a < verticeNum; a++){ //遍歷所有和nowVertice相鄰的節點 79 if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) { //沒有訪問過 80 visitedCount[a] = 1; //記為標記過 81 tallyQueue.offer(a); //加入隊列,上面會繼續取出.來遍歷 82 edges.offer(verticeInfo[a]); //記錄 83 } 84 } 85 } 86 } 87 return edges; 88 } 89 90 /** 91 * 尋找沒有被訪問過的頂點. 92 * @return > 0 即為還未被訪問過的頂點. -1 說明所有的節點都被訪問過了. 93 */ 94 private int findNotVisited(){ 95 for(int i = 0; i < verticeNum; i ++){ 96 if(visitedCount[i] == 0){ 97 return i; 98 } 99 } 100 return -1; 101 } 102 103 /** 104 * 將記錄訪問的數組初始化為0 105 */ 106 private void initVisitedCount(){ 107 for(int i = 0; i < visitedCount.length; i ++){ 108 visitedCount[i] = 0; 109 } 110 } 111 }
下面測試類:
package com.henvealf.datastructures.graph.arcs; import java.util.Queue; /** * 圖的測試類 * Created by Henvealf on 16-5-22. */ public class Main { public static void main(String[] args) { int[][] racs = new int[][]{ {0,1,0,1,0,}, {1,0,1,0,1,}, {0,1,0,1,1,}, {1,0,1,0,0,}, {0,1,1,0,0,}, }; int oo = Integer.MAX_VALUE; int[][] racs1 = new int[][]{ {oo, 1, 1,oo,oo,oo,oo,oo}, { 1,oo,oo, 1,oo,oo,oo,oo}, { 1,oo,oo, 1, 1,oo,oo,oo}, {oo, 1, 1,oo,oo, 1, 1,oo}, {oo,oo, 1,oo,oo,oo,oo, 1}, {oo,oo,oo, 1,oo,oo, 1,oo}, {oo,oo,oo, 1,oo, 1,oo,oo}, {oo,oo,oo,oo, 1,oo,oo,oo}, }; String[] verticeInfos1 = new String[] { "V0","V1","V2","V3","V4","V5","V6","V7" }; Graph<String> graph = new Graph<>(racs2,verticeInfos2); Queue<String> dr = graph.depthFirstSearch(); Queue<String> br = graph.breadthFirstSearch(); System.out.println("--遍歷"); System.out.println("----深度優先結果: " + dr); System.out.println("----廣度優先結果: " + br); }
不足之處,請多多指出,感激不盡.
End... By Henvealf/自安