[原]資料結構之圖
本文是資料結構與演算法之美的學習筆記
圖的概念
圖跟樹一樣也是一種非線性的資料結構,比樹更加複雜一點。
樹種的元素叫做結點,圖中的每個元素叫做頂點,圖中的每一個元素都可以與其他的頂點建立連線關係,這種關係叫做邊。
比如微信,每個使用者就可以叫做一個頂點,兩個好友之間的通道就是邊,每個使用者的好友數量就叫做頂點的度,度表示一個頂點有幾條邊。

微博跟微信不一樣,微博可以單向關注,A關注B,B可以不關注A,這樣每個邊上就可以有一個箭頭了,這種圖叫做有向圖,沒有箭頭的叫做無向圖。

對於有向圖,度分為入度和出度,箭頭指向自己就是入度,箭頭從自己開始指向別的頂點就是出度,對應到微博中就是自己的粉絲和自己關注的人。
如果每條邊上都加上權重,這種圖就叫做帶權圖,比如qq空間中的親密度。

鄰接矩陣(Adjacency Matrix)
鄰接矩陣是圖的一種很直觀的儲存方法,鄰接矩陣的底層依賴的是一個二維陣列
- 對於無向圖來說,如果如果定點i和定點j之間有一條邊,那麼A[i][j]和A[j][i]都會標記為1
- 對於有向圖來說,比如頂點i指向頂點j,那麼A[i][j]就標記為1。
- 對於帶權圖來說就根據邊的權重來填寫了。
使用鄰接矩陣來儲存圖的缺點
- 對於無向圖來說,如果A[i][j]為1那麼A[j][i]肯定也是1,那麼就浪費了一半的儲存空間
- 如果一個圖的頂點很多,但是頂點上的邊很少,比如微信有幾億使用者,每個使用者的好友幾百幾千,這時候使用鄰接矩陣那麼大部分的空間都浪費了
使用鄰接矩陣來儲存圖的優點
- 儲存簡單,在獲取兩個頂點的關係的時候高效
- 可以將一些圖的運算轉換成矩陣的運算
鄰接表(Adjacency List)
由於鄰接矩陣比較浪費空間,我們可以使用鄰接表
鄰接表有點像散列表,每個頂點對應一條連結串列,每個連結串列中儲存了和這個頂點相連的其他的頂點。
鄰接矩陣雖然浪費空間,但是查詢起來比較快,鄰接表節省空間,但是如果我們要知道頂點A是否和頂點B相關聯,我們需要遍歷頂點B的連結串列。這其實就是空間換時間或者時間換空間,真實專案中根據實際情況選擇使用哪一種。
跟散列表的優化一樣,我們可以吧每個頂點對應的連結串列換成平衡二叉查詢樹或者跳錶等比較高效的資料結構來提升其查詢的效率。
使用鄰接表實現的一個無向圖:
public class Graph { private int v; // 頂點的個數 private LinkedList<Integer> adj[]; // 鄰接表 public Graph(int v) { this.v = v; adj = new LinkedList[v]; for (int i=0; i<v; ++i) { adj[i] = new LinkedList<>(); } } public void addEdge(int s, int t) { // 無向圖一條邊存兩次 adj[s].add(t); adj[t].add(s); } }
廣度優先搜尋演算法
廣度優先搜尋演算法(Breadth First Search)是一種基於圖資料結構的演算法,簡稱BFS,原理很簡單就是一層一層的推進,先查詢離起始點最近的,然後查詢次近的,這樣依次往外搜尋。
程式碼實現:
public void bfs(int s, int t) { if (s == t) return; boolean[] visited = new boolean[v]; visited[s]=true; Queue<Integer> queue = new LinkedList<>(); queue.add(s); int[] prev = new int[v]; for (int i = 0; i < v; ++i) { prev[i] = -1; } while (queue.size() != 0) { int w = queue.poll(); for (int i = 0; i < adj[w].size(); ++i) { int q = adj[w].get(i); if (!visited[q]) { prev[q] = w; if (q == t) { print(prev, s, t); return; } visited[q] = true; queue.add(q); } } } } private void print(int[] prev, int s, int t) { // 遞迴列印 s->t 的路徑 if (prev[t] != -1 && t != s) { print(prev, s, prev[t]); } System.out.print(t + " "); }
程式碼中s代表起始頂點,t代表終止頂點,我們搜尋一條從s到t的路徑
visited:用來記錄已經被訪問的頂點,防止頂點被重複訪問。
queue:佇列,用來儲存已經被訪問但是其相連線的頂點還沒被訪問的的頂點,廣度優先搜尋是一層一層的搜尋,queue中記錄的也就是我們當前搜尋的層中的頂點。
prve: 用來記錄搜尋路徑,比如prve[w]記錄的是從哪個前驅頂點遍歷過來的。
深度優先搜尋演算法
深度優先搜尋演算法(Depth First Search)簡稱DFS,文中舉的例子很形象:走迷宮,走到岔路口就選一個進去,走不通的時候就退回到上一個岔路口,從新選一個入口進入,如此迴圈直到找到出口。
程式碼實現:
boolean found = false; // 全域性變數或者類成員變數 public void dfs(int s, int t) { found = false; boolean[] visited = new boolean[v]; int[] prev = new int[v]; for (int i = 0; i < v; ++i) { prev[i] = -1; } recurDfs(s, t, visited, prev); print(prev, s, t); } private void recurDfs(int w, int t, boolean[] visited, int[] prev) { if (found == true) return; visited[w] = true; if (w == t) { found = true; return; } for (int i = 0; i < adj[w].size(); ++i) { int q = adj[w].get(i); if (!visited[q]) { prev[q] = w; recurDfs(q, t, visited, prev); } } }
visited和prev和廣度的作用是一樣的
found的作用是為了跳出遞迴的條件。