摘要:樹是一副無環連通圖。互不相連的樹組成的集合稱為森林。表示無向圖的數據類型圖的基本操作的兩個構造,得到頂點數和邊數,增加一條邊。該方法不符合第一個條件,上百萬個頂點的圖是很常見的空間不滿足。
四種重要的圖模型:
無向圖(簡單連接)
有向圖(連接有方向性)
加權圖(連接帶有權值)
加權有向圖(連接既有方向性又帶有權值)
無向圖定義:由一組頂點和一組能夠將兩個頂點相連的邊組成。
特殊:自環(一條連接一個頂點和其自身的邊);平行邊(連接同一對頂點的兩條邊)
數學家將含有平行邊的圖稱為多重圖;將沒有平行邊或自環的圖稱為簡單圖。現實當中,兩點就可以指代一條邊。
術語表兩個頂點通過一條邊相連,稱這兩頂點相鄰,并稱該連接依附于這兩個頂點。
某個頂點的度數即為依附于它的邊的總數。
子圖是由一幅圖的所有邊的一個子集(以及它們所依附的所有頂點)組成的圖。
路徑是由邊順序連接的一系列頂點。
簡單路徑是一條沒有重復頂點的路徑。
環是一條至少含有一條邊且起點和終點相同的路徑。
簡單環是一條(除了起點和終點必須相同之外)不含有重復頂點和邊的環。
路徑或者環的長度為其中所包含的邊數。
大多情況研究簡單環和簡單路徑,并會省略簡單二字。當允許重復的頂點時,指的都是一般的路徑和環。
當兩個頂點之間存在一條連接雙方的路徑時,稱一個頂點和另一個頂點是連通的。
U-V-W-X記為U到X的一條路徑;U-V-W-X-U記為U到V到W到X再回到U的一條環。
從任意一個頂點都存在一條路徑到達另一個任意頂點,稱這幅圖是連通圖。
一副非連通的圖由若干連通的部分組成,它們都是其極大連通子圖。
直觀上:如果頂點是念珠,邊是連接念珠的線,它們都是物理存在的對象,那么將任意頂點提起來,連通圖都將是一個整體,而非連通圖則會變成兩個或多個部分。
一般來說:要處理一張圖就要一個個地處理它的連通分量(子圖)。
無環圖:不包含環的圖。
樹是一副無環連通圖。互不相連的樹組成的集合稱為森林。連通圖的生成樹是它的一副子圖,它含有圖中的所有頂點且是一棵樹。圖的生成樹森林是它的所有連通子圖的生成樹的集合。
樹的定義非常通用,稍作改動就可以變成用來描述程序行為的(函數調用層次)模型和數據結構(二叉查找樹、2-3樹等)。
當且僅當一幅含有V個節點的圖G滿足下列5個條件之一時,它就是一棵樹:
G有V-1條邊且不含有環;
G有V-1條邊且是連通的;
G是連通的,但刪除任意一條邊都會使之不再連通;
G是無環圖,但添加任意一條邊都會產生一條環;
G中的任意一對頂點之間僅存在一條簡單路徑。
圖的密度是指已經連接的頂點對占所有可能被連接的的頂點對的比例。一般來說,如果一幅圖中不同的邊的數量只占頂點總數V的一小部分,那么就認為這幅圖是稀疏的,否則是稠密的。
二分圖是一種能夠將所有節點分為兩部分的圖,其中圖的每條邊所連接的兩個頂點都分別屬于不同的部分。二分圖會出現在許多場景中。
表示無向圖的數據類型圖的基本操作的API:
兩個構造,得到頂點數V( )和邊數E( ),增加一條邊addEdge( int v, int w )。本節所有算法都基于adj( )方法所抽象的基本操作。第二個構造函數接受的輸入由2E+2個整數組成:首先是V, 然后是E, 在然后是 E 對 0到V-1之間的整數,每個整數對都表示一條邊。
圖的幾種表示方法要面對的下一個圖處理問題就是用哪種數據結構來表示并實現這份API,這包含兩個要求:
必須為可能在應用中碰到的各種類型的圖預留出足夠的空間;
實例方法的實現一定要快—它們是開發處理圖的各種用例的基礎。
要求比較模糊,但是仍然能幫我們在三種圖的表示方法中進行選擇。
鄰接矩陣。用V*V的布爾矩陣,當V和W有邊時,定義V行W列元素為TRUE,否則為FALSE。該方法不符合第一個條件,上百萬個頂點的圖是很常見的.V^2空間不滿足。
邊的數組。可以使用一個Edge類,含有兩個int實例變量。表示方法簡單但是不滿足第二個條件—要實現adj( )需要檢查所有邊。
鄰接表數組。可以使用一個以頂點為索引的列表數組,其中每個元素都是和該頂點相鄰的頂點列表。該結構同時滿足兩個條件。本章一直用它。
除了性能目標,還發現:允許存在平行邊相當于排除了鄰接矩陣,因為鄰接矩陣無法表示它們。
鄰接表的數據結構非稠密圖的標準表示稱為鄰接表的數據結構,它將每個頂點的所有相鄰頂點都保存在該頂點對于的元素所指向的一張鏈表中。使用這個數組就是為了快速訪問給定頂點的鄰接頂點列表。
使用Bag抽象數據類型(也可用Java中的
這種Graph的實現的性能:
使用的空間和V+E成正比;
添加一條邊所需要的時間為常數;
遍歷頂點V的所有相鄰頂點所需要的時間和V的度數成正比。
對于這樣的操作,這樣的特性已經是最優,可以滿足圖處理應用的需要,并且支持平行邊和自環。邊的插入順序決定了Graph得鄰接表中頂點的出現順序。使用構造函數從標準輸入中讀入一副圖時,就意味著輸入的格式和邊的順序決定了Graph的鄰接表數組中頂點的出現順序。
/** * 無向圖 */ public class Graph { private int vertexCount; // 頂點數 private int edgeCount; // 邊數 private LinkedList[] adj; // 鄰接表數組 public Graph(int v){ this.adj = new LinkedList[v]; for(int i = 0; i ();// 初始化鄰接表數組 this.vertexCount = v; } public Graph(In in) { this(in.readInt()); int e = in.readInt();//得到邊數 // 讀取每條邊,進行圖的初始化操作 for(int i = 0; i adj(int v){return adj[v];} /** 把圖轉化成標準字符串形式*/ public String toString(){ String NEWLINE = System.getProperty("line.separator"); StringBuilder sb = new StringBuilder(); sb.append("vertex count: ").append(getVertexCount()) .append(" edge count: ").append(getEdgeCount()) .append(Config.NEWLINE); for (int v = 0; v < getVertexCount();v++){ LinkedList list = adj(v); sb.append(v).append(": ").append("["); for (int i=0; i < list.size();i++){ sb.append(list.get(i)).append(","); } sb.deleteCharAt(sb.length() - 1); sb.append("]").append(NEWLINE); } return sb.toString(); } public static void main(String[] args) { String dir = Graph.class.getPackage().getName().replace(".", "/"); String path = Graph.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); System.out.println(g.toString()); } }
/** * 圖的基本常用操作工具類 */ public class GraphUtils { /** 計算頂點v的度數*/ public static int degree(Graph graph, int v){return graph.adj(v).size();} /** 計算圖中最大的度*/ public static int maxDegree(Graph graph){ int max = 0; for(int i = 0;imax ? currentDegree : max; } return max; } /** 計算圖的平均度數*/ public static int avgDegree(Graph g){ return 2 * g.getEdgeCount() / g.getVertexCount(); } /** 計算自環的個數*/ public static int numberOfSelfLoops(Graph g){ int count = 0; for(int v = 0; v < g.getVertexCount(); v++) for(int w: g.adj(v)) if(v == w) count++; return count / 2; // 每條邊計算了兩次 } public static void main(String[] args) { String dir = GraphUtils.class.getPackage().getName().replace(".", "/"); String path = GraphUtils.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); for (int i = 0; i < g.getVertexCount(); i++) { System.out.println(i+" degree : "+GraphUtils.degree(g, i)); } System.out.println("the max degree is : " + GraphUtils.maxDegree(g)); System.out.println(g.toString()); System.out.println("avg degree: "+GraphUtils.avgDegree(g)); System.out.println("count of self loop: "+GraphUtils.numberOfSelfLoops(g)); } }
0 degree : 4 1 degree : 1 2 degree : 1 3 degree : 2 4 degree : 3 5 degree : 3 6 degree : 2 7 degree : 1 8 degree : 1 9 degree : 3 10 degree : 1 11 degree : 2 12 degree : 2 the max degree is : 4 vertex count: 13 edge count: 13 0: [5,1,2,6] 1: [0] 2: [0] 3: [4,5] 4: [3,6,5] 5: [0,4,3] 6: [4,0] 7: [8] 8: [7] 9: [12,10,11] 10: [9] 11: [12,9] 12: [9,11] avg degree: 2 count of self loop: 0圖的處理算法的設計模式
將圖的表示和實現分離開。為每個任務創建一個相應的類,用例可以創建相應的對象來完成任務。
深度優先搜索探索迷宮方法:tremaux搜索:
選擇一條沒有標記過的通道,在走過的路上鋪一條繩子;
標記所有你第一次路過的路口和通道;
當來到一個標記過的路口時(用繩子)回退到上一個路口;
當回退到得路口已經沒有可走的通道時繼續回退。
繩子可保證總能找到一條出路,標記則能保證不會兩次經過同一條通道或同一個路口。
看Java代碼實現:
/** * 圖的深度優先搜索算法 */ public class DepthFirstSearch { private int count; private boolean[] marked; // 數組存儲每個頂點是否被遍歷過 /** * 從頂點s開始對g進行深搜 * @param g * @param s */ public DepthFirstSearch(Graph g, int s) { marked = new boolean[g.getVertexCount()]; dfs(g, s); } /** 深搜*/ private void dfs(Graph g, int s) { marked[s] = true; // 1.標記頂點s count++; // 2.count數加一 LinkedListlist = g.adj(s);// 3.獲取s的鄰接表 for(int w: list) // 4.對鄰接表進行遍歷 if(!isMarked(w)) dfs(g,w); // 5.如果遍歷到的頂點沒有被標記過,對該頂點繼續遞歸深搜 } /** 頂點w是否和起點s相連通*/ public boolean isMarked(int w){return marked[w];} /** 與起點s連通的頂點數量*/ public int count(){return count;} public static void main(String[] args) { String dir = DepthFirstSearch.class.getPackage().getName().replace(".", "/"); String path = DepthFirstSearch.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstSearch search = new DepthFirstSearch(g, start); System.out.print("start vertex: "+ start+". "); StringBuilder sb = new StringBuilder(); for(int i = 0; i< g.getVertexCount(); i++) if(search.isMarked(i)) sb.append(" "+ i); System.out.println("Connected " + sb.toString()); // 如果和s連通的頂點數量和圖的頂點數量相同,說明是連通圖 if(search.count() == g.getVertexCount()) System.out.println("g is a connected graph."); else System.out.println("g is not a connected graph."); } }
start vertex: 0. Connected 0 1 2 3 4 5 6 g is not a connected graph.
“兩個給定頂點是否連通?”等價于“兩個給定的頂點之間是否存在一條路徑”,也叫路徑檢測問題。
union-find算法的數據結構并不能解決找出這樣一條路徑問題,DFS是已經學習過的方法中第一個能夠解決該問題的算法
能解決的另一問題:單點路徑----給定一幅圖和一個起點s,“從S到給定的頂點V是否存在一條路徑,如果有,找出”
尋找路徑構造函數接受一個起點S作為參數,計算S到與S連通的每個頂點之間的路徑。在為S創建了Paths對象后,用例可以調用pathTo()實例方法來遍歷從S到任意和S連通的頂點的路徑上的所有頂點。以后會實現只查找具有某些屬性的路徑。
Java實現
/** * 深搜尋找路徑問題 */ public class DepthFirstPaths { private boolean[] marked; private int[] edgeTo; // 路徑 private int start; // 起點 public DepthFirstPaths(Graph g, int s){ marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; this.start = s; dfs(g, s); } private void dfs(Graph g, int s) { marked[s] = true; for(int w: g.adj(s)){ if(!marked[w]){ // 如果w沒有被標記過,把路徑數組中的w處置為s,意思:從s到達了w。此處記錄了每一次深搜的路徑節點 edgeTo[w] = s; dfs(g, w); } } } /** 從起點s到頂點v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} public StackpathTo(int v){ if(!hasPathTo(v)) return null; Stack stack = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) // 從終點開始,倒著找起點,依次push入棧 stack.push(x); stack.push(start);// for循環到起點處終止,所以在循環結束后要把起點入棧,至此 一條完整的路徑依次入棧 return stack; } public static void main(String[] args) { String dir = DepthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = DepthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstPaths pathSearch = new DepthFirstPaths(g, start); StringBuilder sb = new StringBuilder(); for(int i = 0; i p = pathSearch.pathTo(i); while(!p.isEmpty()) sb.append(p.pop()).append("->"); sb.deleteCharAt(sb.length()-1); sb.deleteCharAt(sb.length()-1); System.out.println(sb.toString()); } } }
0 to 1: 0->1 0 to 2: 0->2 0 to 3: 0->5->4->3 0 to 4: 0->5->4 0 to 5: 0->5 0 to 6: 0->5->4->6 0 to 7 : not connected. 0 to 8 : not connected. 0 to 9 : not connected. 0 to 10 : not connected. 0 to 11 : not connected. 0 to 12 : not connected.廣度優先搜索BFS
深搜得到的路徑不僅取決于圖的結構,還取決于圖的表示和遞歸調用的性質。我們自然對最短路徑感興趣:
單點最短路徑。給定一幅圖和一個起點S,從S到給定頂點V是否存在一條路徑?如果有,請找出其中最短的那條(所含邊數最少)。
DFS遍歷圖的順序和找出最短路徑的目標無關。
BFS為了這個目標而出現。要找到從S到V得最短路徑,從S開始,在所有由一條邊就可以到達的頂點中查找V, 如果找不到就繼續在與S距離兩條邊的所有頂點中查找,如此一直執行。
DFS好像是一個人在走迷宮,BFS則像一組人在一起朝各個方向走這個迷宮,每個人都有自己的繩子,當出現新的叉路時,可以假設一個探索者可以分裂為更多的人來搜索。當來個那個探索者相遇的時候,合二為一,并繼續使用先到達者的繩子。
在程序中,搜索一幅圖時遇到有多條邊需要遍歷的情況,我們會選擇其中一條并將其他通道留到以后再繼續搜索。在DFS中,用了一個可以下壓的棧,以支持遞歸搜索。使用LIFO的規則來描述壓棧和走迷宮時先探索相鄰的通道類似。從有待搜索的通道中選擇最晚遇到過的那條。
在BFS中希望按照與起點的距離的順序來遍歷所有的頂點:使用FIFO先進先出隊列來代替棧LIFO后進先出 即可。將從有待搜索的通道中選擇最早遇到的那條。
實現:
算法4.2實現了BFS。使用隊列保存所有已經被標記過但其鄰接表還未被檢查過的頂點。先將起點加入隊列,然后重復下面步驟直到隊列為空:
取隊列中的下一個頂點V并標記它;
將與V相鄰的所有未被標記過的頂點加入隊列。
算法4.2中的方法不是遞歸的,不像遞歸中隱式使用的棧,而是顯式地使用了一個隊列。
從隊列中刪除0,將相鄰頂點2 1 5加入隊列,標記它們并分別將它們在edgeTo[ ]中的值置為0;隊列: 0 2 1 5
從隊列中刪除2,并檢查相鄰頂點0 1 3 4, 0和1已經被標記,將3和4這兩個沒被標記的加入隊列,標記它們,并分別將它們在edgeTo[ ] 中的值設為2;隊列: 0 2 1 5 3 4
刪除1,檢查相鄰點0 2,發現都已經被標記;隊列: 0 2 1 5 3 4
刪除5, 檢查相鄰點 0 3, 發現都已經被標記;隊列: 0 2 1 5 3 4
刪除3, 檢查相鄰點 2 4 5, 發現都已經被標記;隊列: 0 2 1 5 3 4
刪除4, 檢查相鄰點 2 3, 發現都已經被標記;隊列: 0 2 1 5 3 4
/** * 廣搜找到最短路徑 * 對于從s可達的任意頂點v,廣搜都能找到一條從s到v的最短路徑 * (沒有其他從s到v的路徑所含邊比這條路徑更少) * 廣搜所需時間在最壞情況下和(v + e)成正比。 */ public class BreadthFirstPaths { private boolean[] marked; private int[] edgeTo; private int start; public BreadthFirstPaths(Graph g, int s){ this.start = s; marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; bfs(g, s); } private void bfs(Graph g, int s) { Queuequeue = new Queue<>(); marked[s] = true; // 標記起點 queue.enqueue(s); // 起點入隊 while(!queue.isEmpty()){ int head = queue.dequeue(); // 從隊列中取出隊首 LinkedList list = g.adj(head); // 得到隊首的鄰接表 for(int w: list){ //遍歷鄰接表 if(!marked[w]){ // 若當前節點沒有被標記過 edgeTo[w] = head; // 1.存入路徑 marked[w] = true; // 2.進行標記 queue.enqueue(w); // 3.節點入隊 } } } } /** 從起點s到頂點v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} /** 返回從起點s到頂點v的一條最短路徑*/ public Stack pathTo(int v){ if(!hasPathTo(v)) return null; // 若不存在到v的路徑,返回Null Stack path = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) path.push(x); path.push(start); return path; } public static void main(String[] args) { String dir = BreadthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = BreadthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 5; BreadthFirstPaths bfPath = new BreadthFirstPaths(g, start); for(int i = 0; i p = bfPath.pathTo(i); while(!p.isEmpty()){ sb.append(p.pop() + "->"); } sb.deleteCharAt(sb.length() - 1); sb.deleteCharAt(sb.length() - 1); System.out.println(sb.toString()); } } }
5 to 0 : 5->0 5 to 1 : 5->0->1 5 to 2 : 5->0->2 5 to 3 : 5->3 5 to 4 : 5->4 5 to 6 : 5->0->6 5 to 7 : not connected. 5 to 8 : not connected. 5 to 9 : not connected. 5 to 10 : not connected. 5 to 11 : not connected. 5 to 12 : not connected.
對于這個例子,edgeTo[]數組在第二步之后就已經完成了。和深搜一樣,一點所有頂點都已經被標記,余下的計算工作就只是在檢查連接到各個已被標記的頂點的邊而已。
命題:對于從S可達到的任意頂點V, 廣搜都能找到一條從S到V的最短路徑(沒有其他從S到V得路徑所含的邊比這條路徑更少)
續: 廣搜所需的時間在最壞情況下和V+E成正比。
DFS和BFS都會先將起點存入數據結構中,然后重復以下步驟知道數據結構被清空:
取其中的下一個頂點并標記它;
將V的所有相鄰而又未被標記的頂點加入數據結構。
不同之處在于從數據結構中獲取下一個頂點的規則:廣搜是最早加入的頂點;深搜是最晚加入的頂點。這種差異得到了處理圖的兩種完全不同的視角,無論哪種,所有與起點連通的頂點和邊都會被檢查到。
連通分量深搜下一個直接應用就是找出一幅圖的所有連通分量。API:
CC的實現使用了marked[ ]數組來尋找一個頂點作為每個連通分量中深度優先搜索的起點。遞歸的深搜第一次調用的參數是頂點0,會標記所有與0連通的頂點。然后構造函數中的for循環會查找每個沒有被標記的頂點并遞歸調用dfs來標記和它相鄰的所有頂點。另外,它還使用了一個以頂點作為索引的數組id[ ],將同一個連通分量中的頂點和連通分量的標識符關聯起來。這個數組使得connected( )方法的實現變得十分簡單。
/** * 強連通分量 */ public class CC { private boolean[] marked; private int[] id; private int count; public CC(Graph g){ marked = new boolean[g.getVertexCount()]; id = new int[g.getVertexCount()]; for(int s = 0; s < g.getVertexCount(); s++){ if(!marked[s]){ dfs(g,s); count++; } } } private void dfs(Graph g, int v) { marked[v] = true; id[v] = count; for(int w: g.adj(v)) if(!marked[w]) dfs(g,w); } /** v和w連通嗎*/ public boolean connected(int v, int w) { return id[v] == id[w]; } /** v所在的連通分量的標識符*/ public int id(int v) { return id[v]; } /** 連通分量數*/ public int count() {return count;} public static void main(String[] args) { String dir = CC.class.getPackage().getName().replace(".", "/"); String path = CC.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); CC cc = new CC(g); int m = cc.count(); System.out.println("number of components: "+ m); LinkedList[] components = new LinkedList[m]; for(int i =0;i (); for(int v = 0; v< g.getVertexCount(); v++) components[cc.id(v)].add(v); for(int i=0;i number of components: 3 0 1 2 3 4 5 6 7 8 9 10 11 12其實現基于一個由頂點索引的數組id[ ].若V屬于第i個連通分量,則id[v]的值為i。構造函數會找出一個未被標記的頂點并調用遞歸函數dfs( )來標記并區分出所有和它連通的頂點,如此重復直到所有的頂點都被標記并區分。
命題C:深搜的預處理使用的時間和空間與V+E成正比且可以在常數時間內處理關于圖的連通性查詢。
和union-find算法對比:理論上深搜比union-find快,因為能保證所需時間是常數,而union-find不行;但在實際中,該差異微不足道。union-find更快,因為它不需要完整的構造并表示一幅圖。更重要的是:union-find算法是一種動態算法(在任何時候都能用接近常數的時間檢查兩個頂點是否連通,甚至是在添加一條邊的時候),但深搜就必須對圖進行預處理。
因此,在完成只需要判斷連通性或是需要完成有大量連通性查詢和插入操作混合等類似的任務時,更傾向使用union-find,而深搜更適合實現圖的抽象數據類型,因為能夠更有效的利用已有數據結構。
DFS已經解決了幾個基礎問題。該方法很簡單,遞歸實現使得我們能夠進行復雜的運算并為一些圖的處理問題給出簡潔的解決方法。
下面對兩個問題進行解答:
檢測環:給定的圖是無環圖嗎?
雙色問題:能夠用兩種顏色將圖的所有頂點著色,使得任意一條邊上的兩個端點的顏色都不同嗎?這個問題等價于:這是一幅二分圖嗎?
檢測環解題:/** * 給定的圖是無環圖嗎 * 檢測自環:假設沒有自環,沒有平行邊 */ public class Cycle { private boolean[] marked; private boolean hasCycle; public Cycle(Graph g){ marked = new boolean[g.getVertexCount()]; for(int i = 0;i true false雙色問題解題/** * 雙色問題:能夠用兩種顏色將圖的所有頂點著色,使得任意一條邊上的兩個端點的顏色都不同嗎? * 等價于:判斷是否是二分圖的問題 */ public class TwoColor { private boolean[] marked; private boolean[] color; private boolean isColorable; public TwoColor(Graph g){ isColorable = true; marked = new boolean[g.getVertexCount()]; color = new boolean[g.getVertexCount()]; for(int i = 0; i true false符號圖典型應用中,圖都是通過文件或者網頁定義的,使用的是字符串而非整數來表示和指代頂點。為了適應這樣的應用,定義擁有以下性質的輸入格式:
頂點名為字符串
用指定的分隔符來隔開頂點名(允許頂點名中含有空格)
每一行都表示一組邊的集合,每條邊都連接著這一行的第一個名稱表示的頂點和其他名稱所表示的頂點
頂點總數V和邊的總數E都是隱式定義的。
例子:
API
定義了一個構造來讀取并構造圖,用name( )方法和index( )方法將輸入流中的頂點名和圖算法使用的頂點索引對應起來。
測試用例例子:飛機場routes.txt--輸入機場代碼查找從該機場起飛到達的城市,但這些信息并不是直接從文件中能得到的。
例子:電影movies.txt--輸入一部電影名字得到演員列表。這不過是在照搬文件中對應的行數據,
? 但輸入演員名字 查看其出演的電影列表,相當于反向索引。
? 盡管數據庫的構造是為了將電影名連接到演員,二分圖模型同時也意味著將演員連接到電影名。
? 二分圖的性質自動完成了反向索引。這將成為處理更復雜的和圖有關的問題的基礎。
符號圖的實現SymbolGraph用到了3種數據結構:
一個符號表st,鍵的類型為String(頂點名),值得類型為int(索引);
一個數組keys[ ],用作反向索引,保存每個頂點索引對應的頂點名;
一個Graph對象G,使用索引來引用圖中的頂點。
SymbolGraph會遍歷兩遍數據結構來構造以上數據結構,主要是因為構造Graph對象需要頂點總數V。在典型的實際應用中,在定義圖的文件中指明V和E可能會不方便,從而有了SymbolGraph,這樣就可以方便地在routes.txt或者movies.txt中添加或者刪除條目而不用但系需要維護邊或者頂點的總數。
Java實現
/** * 符號圖 */ public class SymbolGraph { private HashMapmap; // key:頂點名 value:索引 private String[] keys; // 反向索引,保存每個頂點索引對應的頂點名 private Graph g; // 使用索引來引用圖中的頂點 public SymbolGraph(String path, String sp){ map = new HashMap<>(); BufferedReader reader; String line; try { reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){//第一遍,構造索引 String [] vertexs = line.split(sp); for(String s : vertexs) if(!map.containsKey(s)) map.put(s, map.size()); } reader.close(); keys = new String[map.size()]; for(String name: map.keySet()){ // 遍歷map的key,構造頂點名的反向索引 keys[map.get(name)] = name; } g = new Graph(map.size()); line = ""; reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){ // 第二遍,構造圖,將每一行的頂點和該行其他點相連 String[] strs = line.split(sp); int start = map.get(strs[0]);//獲取起點 for(int i = 1; i< strs.length; i++) g.addEdge(start, map.get(strs[i])); } reader.close(); } catch (IOException e) { e.printStackTrace(); } } /** key是一個頂點嗎*/ public boolean contains(String key){return map.containsKey(key);} /** key的索引*/ public int index(String key){return map.get(key);} /** 索引v的頂點名*/ public String name(int v){return keys[v];} /** 隱藏的Graph對象*/ public Graph graph(){return g;} public static void main(String[] args) { String dir = Cycle.class.getPackage().getName().replace(".", "/"); String path = Cycle.class.getClassLoader().getResource(dir+"/routes.txt").getPath(); SymbolGraph sg = new SymbolGraph(path, " "); Graph g = sg.graph(); HashMap map = sg.map; for(Entry s: map.entrySet()){ System.out.println(s.getKey() + "-" +s.getValue()); } System.out.println(g.toString()); String start = "JFK"; if(!sg.contains(start)){ System.out.println("起點"+start + " 不在數據庫."); return; } int s = sg.index(start); BreadthFirstPaths bfs = new BreadthFirstPaths(g, s); String end = "LAS"; if(!sg.contains(end)){ System.out.println("終點"+end + " 不在數據庫."); }else{ int t = sg.index(end); if(!bfs.hasPathTo(t)){ System.out.println(start +" 和 " + end + " 沒有路徑相同."); return; } Stack stack = bfs.pathTo(t); StringBuilder sb = new StringBuilder(); while(!stack.isEmpty()){ sb.append(sg.name(stack.pop())).append(" "); } System.out.println("起點"+start+"到終點"+end+"的路徑為:"); System.out.println(sb.toString()); } } } LAS-9 LAX-8 DFW-5 ORD-2 JFK-0 HOU-4 ATL-7 DEN-3 PHX-6 MCO-1 vertex count: 10 edge count: 18 0: [1,7,2] 1: [0,7,4] 2: [3,4,5,6,0,7] 3: [2,6,9] 4: [2,7,5,1] 5: [6,2,4] 6: [5,2,3,8,9] 7: [0,4,2,1] 8: [6,9] 9: [3,8,6] 起點JFK到終點LAS的路徑為: JFK ORD DEN LAS同樣可以把電影-演員作為例子輸入:
這個Graph實現允許用例用字符串代替數字索引來表示圖中的頂點。
它維護了
實例變量st(符號表用來映射頂點名和索引)
keys(數組用來映射索引和頂點名)
g(使用索引表示頂點的圖)
為了構造這些數據結構,代碼會將圖的定義處理兩遍(定義的每一行都包含一個頂點以及它的相鄰頂點列表,用分隔符sp隔開)
間隔的度數圖處理的一個經典問題就是,找到一個社交網絡之中兩個人間隔的度數。
演員K演過很多電影,為圖中每個演員附一個K數:
K本人為0,
所有和K演過同一部電影的人的值為1,
所有(除K外)和K數為1的演員出演過同一部電影的人的值為2,
以此類推。
可以看到K數必須為最短電影鏈的長度,因此不用計算機,很難知道。
用例DegreesOfSeparation所示,BreadthFirstPaths才是我們所需要的程序,通過最短路徑來找出movies.txt中任意演員的K數。
總結幾個基本概念:
圖的術語;
一種圖的表示方法,能夠處理大型而稀疏的圖;
和圖處理相關的類的設計模式,其實現算法通過在相關的類的構造函數中對圖進行預處理,構造所需的數據結構來高效支持用例對圖的查詢;
DFS&BFS
支持使用符號作為圖的頂點名的類。
上表總結了本節所有圖算法的實現。適合作為圖處理的入門學習。隨后學習復雜類型圖以及更加困難的問題時,會用到這些代碼的變種。
考慮了邊的方向以及權重之后,同樣地問題會變得困難得多,但同樣地算法仍然湊效并將成為解決更復雜問題的起點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66870.html
摘要:邊僅由兩個頂點連接,并且沒有方向的圖稱為無向圖。用分隔符當前為空格,也可以是分號等分隔。深度優先算法最簡搜索起點構造函數找到與起點連通的其他頂點。路徑構造函數接收一個頂點,計算到與連通的每個頂點之間的路徑。 Algorithms Fourth EditionWritten By Robert Sedgewick & Kevin WayneTranslated By 謝路云Chapter...
摘要:本筆記內容參考算法第四版書本大致框架書本分為大部分基礎排序查找圖字符串第一章基礎 本筆記內容參考(第四版) 書本大致框架 showImg(https://segmentfault.com/img/bVXZzA?w=594&h=376);書本分為5大部分: 基礎 排序 查找 圖 字符串 第一章 基礎 showImg(https://segmentfault.com/img/bV...
摘要:前文數據結構與算法常用數據結構及其實現總結了基本的數據結構,類似的,本文準備總結一下一些常見的高級的數據結構及其常見算法和對應的實現以及應用場景,務求理論與實踐一步到位。 前文 數據結構與算法——常用數據結構及其Java實現 總結了基本的數據結構,類似的,本文準備總結一下一些常見的高級的數據結構及其常見算法和對應的Java實現以及應用場景,務求理論與實踐一步到位。 跳躍表 跳躍列表是對...
閱讀 2679·2023-04-25 20:28
閱讀 1849·2021-11-22 09:34
閱讀 3687·2021-09-26 10:20
閱讀 1834·2021-09-22 16:05
閱讀 3085·2021-09-09 09:32
閱讀 2502·2021-08-31 09:40
閱讀 2099·2019-08-30 13:56
閱讀 3320·2019-08-29 17:01