--- title: 'Graph 圖形' disqus: kyleAlien --- Graph 圖形 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: * Graph 無法從單個節點訪問全部節點,但樹可以~ > 最常被應用在`最短路徑搜尋`、`拓樸排序` ## 簡介 * 樹狀結構是在討論 Node 之間的 ==層次== 關係,圖形是討論 Node 之間 ==連線與否==,在圖形中可在相互連線的部分加入**權重** (網路關係) ### 尤拉橋 * 圖形理論是來自,數學家**尤拉** ([Euler](https://zh.wikipedia.org/wiki/%E8%90%8A%E6%98%82%E5%93%88%E5%BE%B7%C2%B7%E6%AD%90%E6%8B%89)),為了解決`肯尼茲保橋梁` (七橋理論) 問題,7 座橫跨 4 個大都市的橋梁 > ![](https://i.imgur.com/OrlV3Vz.png) Euler 得出了以下結論 1. 當所有的分支度 **都是偶數** 時,才能==從原點走過每座橋,**再回原點**==,上圖每個 Node 分之度都是奇數所以不可能 > 上圖分支度 > A city: 5 > B city: 3 > C city: 3 > D city: 3 2. 如果條件改成,==經過每座橋然後**不用回原點**==,亦即**只允許其中兩個 Node 的分支度為奇數,其他 Node 必須是偶數** ### 圖形定義 * 圖形以`頂點` + `邊`組成,其表示法為,圖形名稱=(V, E),**V 為所有頂點的集合,E 為所有邊的集合** ### 無向圖形 * 無向圖形,其 ==**表示法為 (V, E)**==,邊 (A, B) **等於** (B, A) > ![](https://i.imgur.com/eZhOL6a.png) > > **頂點的集合**為,V = {A, B, C, D, E, F} > **邊的集合**為,E = {(A, B), (B ,D), (C, D), (D, E), (D, F), (E, F)} | 數語 | 解釋 | | -------- | -------- | | 完整圖形 | 符合 : 邊條數量 = 頂點數量(N) * (N - 1) **/ 2**,每個頂點的分支數為 N - 1 | | 子圖 | 子圖的定義是,該子圖必定包含在主圖中 | | 循環 | 起點與終點相同 | | 依附 | Node 所有有關的邊,上圖 D 的依附為 (C, D), (D, F), (D, E), (B,D) | | 分支度 | Node 的依附的數量,D 的分之度為 4 | ### 有向圖形 * 有向圖形,其 ==**表示法為 <V, E>**==,邊 (A, B) **不等於** (B, A) > ![](https://i.imgur.com/xzM4VvU.png) > > **頂點的集合**為,V = {A, B, C, D, E} > **邊的集合**為,E = {<A, B>, <B, D>, <B, E>, <C, B>, <D, C>, <E, D>} | 數語 | 解釋 | | -------- | -------- | | 完整圖形 | 符合 : 邊條數量 = 頂點數量(N) * (N - 1),每個頂點的分支數為 N - 1 | | 強連結 | 相互連結, <A, B> && <B, A> | | 強連結單元 | **每個 Node** 間都必須是強連結 | | 出分之度 | 從 Node 發出的數量,上圖 Node D 的出分之度為 1 | | 入分之度 | 進入 Node 的數量,上圖 Node D 的入分之度為 2 | ### 複線圖 * **==圖中任意兩點只能有一條邊==**,如果兩個 Node 之間有 >= 2 條邊,則是複線圖,**++嚴格來說複線圖並不算是圖形++**,要進行 **切割子圖** > 複線圖,尤拉橋 > ![](https://i.imgur.com/PWLcp27.png) > > 換成有子圖的圖形 > ![](https://i.imgur.com/FOQx1Nr.png) ## 圖形表示法 * 在電腦中,常使用的圖形表示法有四種,`相鄰矩陣`、`相鄰串列`、`相鄰複合串列`、`索引表格法` ### 相鄰矩陣 * **圖形有 N 個 Node 就使用 N * N 個二為陣列**,並且有向圖形 & 無向圖形的特性不同 1. 對於**無向圖形**來說,**相鄰==矩陣是對稱的,並且對角線一定為 0==**(不指向自己),**有向圖形**就 ==**不一定是對稱的**== 2. **無向圖形任意 Node i 的分支度就是二為陣列中 i `列` 的加總** 3. **有向圖形任意 Node i 的==出分支度==就是二維陣列中 i `列`的加總,==入分支度==是二維陣列中 i `行`的加總** 4. 以消耗空間來說,**無向圖形**因為對角對稱,**只需[儲存上三角 or 下三角](https://hackmd.io/T3he4PgnRnK7vTtNQwcymw#%E4%B8%89%E8%A7%92%E7%9F%A9%E9%99%A3)的資料即可**,僅需要 n(n-1) / 2 的空間 * **無向圖形** > ![](https://i.imgur.com/dqCBmIs.png) > Node.1 的分支度為,列 1 的總和,0 + 1 + 0 + 0 + 1 = 2 > Node.2 的分支度為,列 2 的總和,1 + 0 + 1 + 1 + 0 = 3 > Node.3 的分支度為,列 3 的總和,0 + 1 + 0 + 1 + 1 = 3 > 以此類推... ```java= import java.util.LinkedList; public class Tree_Basic { public static void main(String[] args) { NonDirect(); } public static void NonDirect() { // 14's node to node int[][] data = { {1,2}, {2,1}, {1,5}, {5,1}, {2,3}, {3,2}, {2,4}, {4,2}, {3,4}, {4,3}, {3,5}, {5,3}, {4,5}, {5,4} }; int len = NodeCount(data); Msg("Len is " + len); int[][] newData = new int[len][len]; for(int i = 0; i < data.length; i++) { int x = data[i][0]; int y = data[i][1]; // for each Array for(int j = 1; j <= len; j++) { for(int k = 1; k <= len; k++) { if(j == x && k == y) { newData[x - 1][y - 1] = 1; } } } } } private static int NodeCount(int[][] data) { if(data == null) return 0; LinkedList<Integer> link = new LinkedList<>(); for(int i = 0;i < data.length; i++) { int cache = data[i][0]; // 取正方形,所以只需要比對 data[i][0] 即可以 if(!link.contains(cache)) { link.push(cache); } } return link.size(); } private static void Msg(String str) { if(str == null) return; System.out.println(str); } } ``` **實作** > ![](https://i.imgur.com/EXD8sPu.png) * 有向圖形 > ![](https://i.imgur.com/NOmZpJV.png) > Node.4 的==出分支度==,**==列== 4 的總和**,0 + 0 + 1 + 0 + 1 = 2 > Node.4 的==入分支度==,**==行== 4 的總和**,0 + 1 + 0 + 0 + 1 = 2 > 以此類推... ```java= import java.util.LinkedList; public class Tree_Basic { public static void main(String[] args) { HasDirect(); } public static void HasDirect() { int[][] data = { {1,2}, {2,1}, {2,3}, {2,4}, {3,5}, {4,3}, {4,5}, {5,4} }; int len = DirNodeCount(data); Msg("Len is " + len, true); int[][] newData = new int[len][len]; for(int i = 0; i < data.length; i++) { // for each data array int x = data[i][0]; int y = data[i][1]; for(int j = 0; j < len; j++) { // for each newData array to put data for(int k = 0; k < len; k++) { newData[x - 1][y - 1] = 1; } } } printArray(len, newData); } //"1. " private static int DirNodeCount(int[][] data) { if(data == null) return 0; LinkedList<Integer> link = new LinkedList<>(); for(int i = 0; i < data.length; i++) { int cache = data[i][0]; int cache2 = data[i][1]; if(!link.contains(cache) || !link.contains(cache)) { link.push(cache); } } return link.size(); } private static void printArray(int len, int[][] newData) { for(int j = 0; j < len; j++) { for(int k = 0; k < len; k++) { Msg("[" + newData[j][k] + "]", false); } Msg("", true); } } private static void Msg(String str, boolean ln) { if(str == null) return; if(ln) { System.out.println(str); } else { System.out.print(str); } } } ``` 1. DirNodeCount 作用是取得所有的節點,並返回 Node 數量,與無向圖形不同的地方在 > 無向圖形 : 一定是**相互對應**所以只需取一個值 > 有向圖形 : **可能單方面指向**,所以必須兩個值都取出,Ex: <1, 5> 但 5 並沒有指向外面,會造成掃描不到 Node 5 ### 相鄰串列 * 矩陣的表示法,如果過多 0 的話就形成的[稀疏矩陣](https://hackmd.io/T3he4PgnRnK7vTtNQwcymw#稀疏矩陣),造成空間浪費 > 其缺點也是如同鏈接,搜索會較花時間 * 無邊圖形規則 : n 個 Node 就有 n 個首節點,m 個邊就有 2m 個串鏈結,所有頂點分支度的時間為 O(m+n) > ![](https://i.imgur.com/yi5HMD5.png) > 5 個綠色首節點,5 個末節點,**7個邊,14 個白色串鏈結** ```java= import java.util.LinkedList; public class Tree_Basic { public static void main(String[] args) { NoDirList(); } private static void NoDirList() { int[][] data = { {1,2}, {2,1}, {1,5}, {5,1}, {2,3}, {3,2}, {2,4}, {4,2}, {3,4}, {4,3}, {3,5}, {5,3}, {4,5}, {5,4} }; int count = HeaderCount(data); Msg("Header Count: " + count, true); for(int i = 0; i < count; i++) { MyGraphList mList = new MyGraphList(); for(int j = 0; j < data.length; j++) { int cache = data[j][0]; if(i == (cache - 1)) { mList.push(data[j][1]); } } Msg("Header [" + (i+1) + "]: ", false); mList.printList(); } } private static int HeaderCount(int[][] data) { LinkedList<Integer> list = new LinkedList<>(); for(int i = 0; i < data.length; i++) { int cache = data[i][0]; if(!list.contains(cache)) { list.push(cache); } } return list.size(); } private static void Msg(String str, boolean ln) { if(str == null) return; if(ln) { System.out.println(str); } else { System.out.print(str); } } } class Node { int value; Node next = null; Node(int value) { this.value = value; } } class MyGraphList { Node first, last; MyGraphList() { } boolean isEmpty() { return first == null; } void push(int value) { Node n = new Node(value); if(isEmpty()) { first = n; last = n; return; } else { last.next = n; last = n; } } void printList() { if(isEmpty()) { return; } Node temp = first; while(temp != null) { System.out.print("[" + temp.value + "] "); temp = temp.next; } System.out.println(); } } ``` **--實作--** > ![](https://i.imgur.com/rsCokwC.png) * 有邊圖形規則 : n 個 Node 就有 n 個首節點,m 個邊就有 m 個串鏈結,所有頂點分支度的時間為 O(2n) > ![](https://i.imgur.com/6UecfxS.png) > 5 個綠色首節點,5 個末節點,**8個邊,8 個白色串鏈結** ```java= import java.util.LinkedList; public class Tree_Basic { public static void main(String[] args) { HasDirectList(); } public static void HasDirectList() { int[][] data = { {1,2}, {2,1}, {2,3}, {2,4}, {3,5}, {4,3}, {4,5}, {5,4} }; int count = HeaderCount(data); Msg("Header Count: " + count, true); for(int i = 0; i < count; i++) { MyGraphList mList = new MyGraphList(); for(int j = 0; j < data.length; j++) { int cache = data[j][0]; if(i == (cache - 1)) { mList.push(data[j][1]); } } Msg("Header [" + (i+1) + "]: ", false); mList.printList(); } } private static int HeaderCount(int[][] data) { LinkedList<Integer> list = new LinkedList<>(); for(int i = 0; i < data.length; i++) { int cache = data[i][0]; int cache2 = data[i][1]; if(!list.contains(cache) || !!list.contains(cache2)) { list.push(cache); } } return list.size(); } private static void Msg(String str, boolean ln) { if(str == null) return; if(ln) { System.out.println(str); } else { System.out.print(str); } } } class Node { int value; Node next = null; Node(int value) { this.value = value; } } class MyGraphList { Node first, last; MyGraphList() { } boolean isEmpty() { return first == null; } void push(int value) { Node n = new Node(value); if(isEmpty()) { first = n; last = n; return; } else { last.next = n; last = n; } } void printList() { if(isEmpty()) { return; } Node temp = first; while(temp != null) { System.out.print("[" + temp.value + "] "); temp = temp.next; } System.out.println(); } } ``` **--實作--** > ![](https://i.imgur.com/HfpTTfO.png) ### 相鄰複合串列 * 特色是處理邊 ==**邊**==,不像是上方兩個方式是以**頂**的觀點出發 * 相鄰複合串列是處理**無向圖形**,使用的鏈結內容有 5 個 | Mark | V1 | V2 | Link1 | Link2 | | -------- | -------- | -------- | -------- | -------- | | 紀錄 Flag | 邊的**起點** | 邊的**終點** | 連結下一個==未被記錄==並與 V1 相連的點 | 連結下一個==未被記錄==並與 V2 相連的點 | > ![](https://i.imgur.com/sJ2WXBk.png) ### 索引表格法 * 一維陣列來表示儲存與各頂點相鄰的所有 Node > ![](https://i.imgur.com/Dn8vQAs.png) ## 圖形遍歷 * 圖形遍歷有分,深度優先(DSF)、廣度優先(BSF) ### Depth Search Frist (DSF) * **使用到 `堆疊 Stack`、`遞歸` 的概念** ```java= import java.util.LinkedList; public class Tree_Dsf { //"1. " static DsfTreeStack[] myArray; //"2. " static boolean MarkStamp[]; public static void main(String[] args) { int[][] data = { {1,2}, {2,1}, {1,5}, {5,1}, {2,3}, {3,2}, {2,4}, {4,2}, {3,4}, {4,3}, {3,5}, {5,3}, {4,5}, {5,4} }; /* int[][] data = { {1,2}, {2,1}, {1,3}, {3,1}, {2,4}, {4,2}, {2,5}, {5,2}, {3,6}, {6,3}, {3,7}, {7,3}, {4,5}, {5,4}, {6,7}, {7,6}, {5,8}, {8,5}, {6,8}, {8,6} }; */ int count = HeaderCount(data); Msg("Header Count: " + count, true); myArray = new DsfTreeStack[count]; MarkStamp = new boolean[count]; for(int i = 0; i < count; i++) { myArray[i] = new DsfTreeStack(); for(int j = 0; j < data.length; j++) { int cache = data[j][0]; if((cache - 1) == i) { myArray[i].push(data[j][1]); } } myArray[i].printBsf(); } Msg("深度走訪: ", true); // start point is 1 "3. " dfs(1); } private static void dfs(int current) { MarkStamp[current - 1] = true; Msg("[" + current + "] ", false); DsfNode temp = myArray[current - 1].first; while(temp != null) { int v = temp.value; if(!MarkStamp[v - 1]) { dfs(temp.value); } temp = temp.next; } } private static int HeaderCount(int[][] data) { LinkedList<Integer> list = new LinkedList<>(); for(int i = 0; i < data.length; i++) { int cache = data[i][0]; if(!list.contains(cache)) { list.push(cache); } } return list.size(); } private static void Msg(String str, boolean ln) { if(str == null) return; if(ln) { System.out.println(str); } else { System.out.print(str); } } } // 使用相鄰串列 class DsfNode { int value; DsfNode next; DsfNode(int value) { this.value = value; next = null; } } class DsfTreeStack { DsfNode first, last; boolean isEmpty() { return first == null; } void push(int value) { DsfNode n = new DsfNode(value); if(isEmpty()) { first = n; last = n; return; } else { last.next = n; last = n; } } void printBsf() { DsfNode cache = first; while(cache != null) { System.out.print("[" + cache.value + "]"); cache = cache.next; } System.out.println(""); } } ``` 1. DsfTreeStack 以**相鄰串列**的方式表達圖形 2. MarkStamp 做為頂點已走過的標記,**每走過一個 Node 皆需要 Mark,因為走過的頂點不能再走** 3. 開始走訪的 Node 此範例定為 1 (也可按照自己的意思從不同節點走) **--實做--** > ![](https://i.imgur.com/LuY12VY.png) ### Breadth Search First (BSF) * **使用到 `佇列 Queue` 的概念** ```java= package dataStruct; import java.util.LinkedList; public class Tree_Bsf { static BsfTreeLink[] myLink; static boolean[] MarkStamp; public static void main(String[] args) { int[][] data = { {1,2}, {2,1}, {1,5}, {5,1}, {2,3}, {3,2}, {2,4}, {4,2}, {3,4}, {4,3}, {3,5}, {5,3}, {4,5}, {5,4} }; /* int[][] data = { {1,2}, {2,1}, {1,3}, {3,1}, {2,4}, {4,2}, {2,5}, {5,2}, {3,6}, {6,3}, {3,7}, {7,3}, {4,5}, {5,4}, {6,7}, {7,6}, {5,8}, {8,5}, {6,8}, {8,6} }; */ int count = HeaderCount(data); Msg("Header Count: " + count, true); myLink = new BsfTreeLink[count]; MarkStamp = new boolean[count]; for(int i = 0; i < count; i++) { myLink[i] = new BsfTreeLink(); for(int j = 0; j < data.length; j++) { int head = data[j][0]; if(i == (head - 1)) { myLink[i].push(data[j][1]); } } myLink[i].printBsf(); } Msg("廣度走訪: ", true); // start point is 1 bfs(1, count); } private static void bfs(int current, int size) { BsfQueue queue = new BsfQueue(size); MarkStamp[current - 1] = true; queue.enqueue(current); System.out.print("[" + current +"] "); while(queue.front != queue.rear) { current = queue.dequeue(); //"1. " BsfNode temp = myLink[current - 1].first; while(temp != null) { if(!MarkStamp[temp.value - 1]) { //"2. " MarkStamp[temp.value - 1] = true; //"3. " queue.enqueue(temp.value); System.out.print("[" + temp.value +"] "); } temp = temp.next; } } } private static int HeaderCount(int[][] data) { LinkedList<Integer> list = new LinkedList<>(); for(int i = 0; i < data.length; i++) { int cache = data[i][0]; if(!list.contains(cache)) { list.push(cache); } } return list.size(); } private static void Msg(String str, boolean ln) { if(str == null) return; if(ln) { System.out.println(str); } else { System.out.print(str); } } } //使用相鄰串列 class BsfNode { int value; BsfNode next; BsfNode(int value) { this.value = value; next = null; } } class BsfQueue { int front = -1; int rear = -1; private int[] array; BsfQueue(int size) { array = new int[size]; } void enqueue(int value) { if(array.length <= rear) { System.out.println("array full, cannot enqueue"); return; } array[++rear] = value; } int dequeue() { if(rear == front) { System.out.println("array empty, cannot dequeue"); return -1; } return array[++front]; } } class BsfTreeLink { BsfNode first, last; boolean isEmpty() { return first == null; } void push(int value) { BsfNode n = new BsfNode(value); if(isEmpty()) { first = n; last = n; return; } else { last.next = n; last = n; } } void printBsf() { BsfNode cache = first; while(cache != null) { System.out.print("[" + cache.value + "]"); cache = cache.next; } System.out.println(""); } } ``` 1. 取出當前的頂點,-1 是為了符合 array 在電腦中的 index 2. 當前 Node 並未走訪時就開始走訪,開始走訪前先標註 Mark 3. 走訪過的結點壓入 Queue **--實做--** > ![](https://i.imgur.com/UfjngTP.png) ## Appendix & FAQ :::info 相鄰複合串列的規則跟圖有點問題 ? 座標 m5(3->4)/m5(4->3) 可以反向指,但是為何 m6 不能反向指 m2(5->1) 不行 ? ::: ###### tags: `資料結構`