Algorithms Fourth Edition
Written By Robert Sedgewick & Kevin Wayne
Translated By 謝路雲
Chapter 4 Section 1 無向圖html
如下內容修改自
http://www.cnblogs.com/skyivb...
http://www.cnblogs.com/yangec...
http://blog.csdn.net/yafeicha...java
圖是若干個頂點(Vertices)和邊(Edges)相互鏈接組成的。邊僅由兩個頂點鏈接,而且沒有方向的圖稱爲無向圖。 在研究圖之算法
前,有一些定義須要明確,下圖中表示了圖的一些基本屬性的含義,這裏就很少說明。數組
鄰接列表數據結構
鄰接矩陣 空間V^2app
邊的數組 要實現adj(),即要知道一個頂點和哪些頂點相鄰,須要遍歷每個邊函數
對於非稠密的無向圖,標準表示是使用鄰接表,將無向圖的每一個頂點的全部相鄰頂點都保存在該頂點對應的元素所指向的一張oop
鏈表中。全部的頂點保存在一個數組中,使用這個數組就能夠快速訪問給定頂點的鄰接頂點列表。下面就是非稠密無向圖的一性能
個例子動畫
這種 Graph 的實現的性能有以下特色:
使用的空間和 V + E 成正比
添加一條邊所需的時間爲常數
遍歷頂點 v 的全部相鄰頂點所需的時間和 v 的度數成正比(處理每一個相鄰頂點所需的時間爲常數)
對於這些操做,這樣的特性已是最優的了,已經能夠知足圖處理應用的須要。
public class Graph { private final int V; // number of vertices 頂點數 這個final值在構造函數中初始化後就不能修改了。 private int E; // number of edges 邊數 private Bag<Integer>[] adj; // adjacency lists 鄰接表數組 將Bag替換成Stack也能夠, adj.add()改成adj.push()。 public Graph(int V) { this.V = V; this.E = 0; adj = (Bag<Integer>[]) new Bag[V]; // Create array of lists. for (int v = 0; v < V; v++) // Initialize all lists adj[v] = new Bag<Integer>(); // to empty. //因爲 Java 語言固有的缺點,沒法建立泛型數組,因此第 10 行中只能建立普通數組後強制轉型爲泛型數組。這致使在編譯時出現警告信息。 //因爲 Java 語言固有的缺點,泛型的參數類型不能是原始數據類型,因此泛型的參數類型是 Integer,而不是 int 。這致使了一些性能損失。 } public Graph(In in) { this(in.readInt()); // Read V and construct this graph. int E = in.readInt(); // Read E. for (int i = 0; i < E; i++) { // A an edge. int v = in.readInt(); // Read a vertex, int w = in.readInt(); // read another vertex, addEdge(v, w); // and add edge connecting them. } } public int V() { return V; } public int E() { return E; } public void addEdge(int v, int w) { adj[v].add(w); // Add w to v's list. adj[w].add(v); // Add v to w's list. E++; } //這裏爲何要返回Iterable?返回Stack<Integer>或者Bag<Integer>能夠嗎? public Iterable<Integer> adj(int v) { return adj[v]; } public String toString() { StringBuilder s = new StringBuilder(); //待研究 StringBuiler類 String NEWLINE = System.getProperty("line.separator"); s.append(V + " vertices, " + E + " edges" + NEWLINE); for (int v = 0; v < V; v++) { s.append(v + ": "); for (int w : adj[v]) s.append(w + " "); s.append(NEWLINE); } return s.toString(); } }
其餘經常使用代碼
// 深度 = 相鄰頂點的個數/鏈接邊的數量 public static int degree(int v) { int degree = 0; for (int w : G.adj(v)) degree++; return degree; } // 最大深度 public static int maxDegree(Graph G) { int max = 0; for (int v = 0; v < G.V(); v++) if (degree(G, v) > max) max = degree(G, v); return max; } // 平均深度 // 一個頂點的深度爲和它相鄰的頂點數=鏈接它的邊數 // 平均深度=求和(每一個頂點的邊數)/頂點數 = 2E/V public static int avgDegree(Graph G) { return 2 * G.E() / G.V(); } //自環數 public static int numberOfSelfLoops(Graph G) { int count = 0; for (int v = 0; v < G.V(); v++) for (int w : G.adj(v)) if (v == w) count++; return count / 2; // each edge counted twice }
引入符號圖是由於,頂點更多的不是數字表示,而是由字符串表示,所以要作一個映射
三種數據結構
符號表 st 鍵爲String(頂點字符串名字), 值爲int(索引數字)
數組keys[] 反向索引(經過數字反過來找到字符串)
圖對象G
輸入數據格式
A B
A D
D E
D B
...
每一行表示一條邊,每一行的兩個字符串表示鏈接邊的兩個頂點。用分隔符(當前爲空格,也能夠是分號等)分隔。
(Graph的建立能夠直接使用符號表,而不使用Bag,使得不須要遍歷文件兩次。待補充。詳情可見An Introduction to Programming in Java: An Interdisciplinary Approach.)
public class SymbolGraph { private ST<String, Integer> st; // String -> index 就是個Map private String[] keys; // index -> String 就是個反向Map private Graph G; // the graph public SymbolGraph(String stream, String sp) { //stream是文件名,sp是分隔符 //下面這一段就是遍歷一遍文件,獲得全部頂點的字符串名,放進Map裏。而後再創建一個數組(反向Map)。這樣就創建了字符串和數字的雙向映射關係。 st = new ST<String, Integer>(); In in = new In(stream); // First pass while (in.hasNextLine()) // builds the index { String[] a = in.readLine().split(sp); // by reading strings for (int i = 0; i < a.length; i++) // to associate each if (!st.contains(a[i])) // distinct string st.put(a[i], st.size()); // with an index. } keys = new String[st.size()]; // Inverted index for (String name : st.keys()) // to get string keys keys[st.get(name)] = name; // is an array. G = new Graph(st.size()); in = new In(stream); // Second pass while (in.hasNextLine()) // builds the graph { String[] a = in.readLine().split(sp); // by connecting the int v = st.get(a[0]); // first vertex for (int i = 1; i < a.length; i++) // on each line G.addEdge(v, st.get(a[i])); // to all the others. } } public boolean contains(String s) { return st.contains(s); } public int index(String s) { return st.get(s); } public String name(int v) { return keys[v]; } public Graph G() { return G; } }
int s:起點
構造函數:找到與起點連通的其餘頂點。在圖中從起點開始沿着路徑到達其餘頂點,並標記每一個路過的頂點。
方法marked(int v):判斷s是否和v相連通
方法count(): 有多少個頂點和起點相連?(相似於G.adj(s)的個數)
在談論深度優先算法以前,咱們能夠先看看迷宮探索問題。下面是一個迷宮和圖之間的對應關係:
迷宮中的每個交會點表明圖中的一個頂點,每一條通道對應一個邊。
迷宮探索能夠採用Trémaux繩索探索法。即:
在身後放一個繩子
訪問到的每個地方放一個繩索標記訪問到的交會點和通道
當遇到已經訪問過的地方,沿着繩索回退到以前沒有訪問過的地方:
圖示以下:
下面是迷宮探索的一個小動畫:
深度優先搜索算法模擬迷宮探索。在實際的圖處理算法中,咱們一般將圖的表示和圖的處理邏輯分開來。因此算法的總體設計
模式以下:
建立一個Graph對象
將Graph對象傳給圖算法處理對象,如一個Paths對象
而後查詢處理後的結果來獲取信息
一條路子走到底,不到南門不回頭。
從一個頂點v出發,遍歷與自身相連通的頂點
在遍歷過程當中,設當前被遍歷的頂點爲w
標記w爲已訪問
判斷w是否已經被訪問
是,則繼續遍歷;
否,則搜索與頂點w相連通的頂點(即調用自身,開始關於頂點w的遍歷)
結束遍歷
下面是深度優先的基本代碼,咱們能夠看到,遞歸調用dfs方法,在調用以前判斷該節點是否已經被訪問過。
public class DepthFirstSearch{ private boolean[] marked; private int count; public DepthFirstSearch(Graph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; count++; for (int w : G.adj(v)) { if (!marked[w]) dfs(G, w); } } public boolean marked(int w) { return marked[w]; } public int count() { return count; } }
上圖中是黑色線條表示 深度優先搜索中,全部定點到原點0的路徑, 他是經過edgeTo[]這個變量記錄的,能夠從右邊能夠看
出,他本質是一顆樹,樹根便是原點,每一個子節點到樹根的路徑便是從原點到該子節點的路徑。
深度優先搜索標記與起點連通的全部頂點所須要的時間和全部頂點的深度之和成正比。
圖是否連通?
從概念上來講,若是一個圖是連通的,那麼對於圖上面的任意兩個節點i, j來講,它們相互之間能夠經過某個路徑鏈接到對方
兩個給定頂點是否連通?
連通份量API
實現1.
ConnectedComponents
算法思路
深度優先搜索,依次創建一棵樹
其預處理時間和V+E成正比
ConnectedComponents 代碼
public class CC { public CC(Graph G) { marked = new boolean[G.V()]; id = new int[G.V()]; for (int s = 0; s < G.V(); s++) //從0開始遍歷 if (!marked[s]) { dfs(G, s); count++; //若是dfs返回了,說明全部和0連通的都找完了。就開始找下一個連通的team了,所以count++ } } private void dfs(Graph G, int v) { marked[v] = true; id[v] = count; //新加 保存id for (int w : G.adj(v)) if (!marked[w]) dfs(G, w); } public boolean connected(int v, int w) { return id[v] == id[w]; } public int id(int v) { return id[v]; } public int count() { return count; }
實現2.
UnionFind 詳情請見Chapter 1.5
給定一副圖G和一個頂點s,從s到給定頂點v是否存在一條路徑?若是有,找出這條路徑。
路徑API
構造函數接收一個頂點s,計算s到與s連通的每一個頂點之間的路徑。
如今暫時查找全部路徑。
DepthFirstPaths 代碼
和DepthFirstSearch幾乎一致。
只是添加了路徑的記錄數組int[] edgeTo
public class DepthFirstPaths { private boolean[] marked; private int[] edgeTo; //新加。第一次訪問頂點v的頂點爲w。 edgeTo[v]=w private final int s; //新加。把圖變成樹,構造時的頂點s爲樹的根結點爲s // private int count; 被刪去了 public DepthFirstPaths (Graph G, int s) { this.s = s; marked = new boolean[G.V()]; edgeTo = new int[G.V()]; dfs(G, s); } private void dfs(Graph G, int v) { // count++; marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) { edgeTo[w] = v; //新加,記錄路徑 dfs(G, w); } } } // 圖變成了樹,樹的根爲s // 圖被扔掉了,以樹的形式保留在數組裏了 public boolean hasPathTo(int v) { return marked[v]; } public Iterable<Integer> pathTo(int v) { if (!hasPathTo[v]) return null; Stack<Integer> path = new Stack<Integer>(); for (int w = v; w != s; w = edgeTo[w]) path.push(w); path.push(s); return path; } }
給定的環是無環圖嗎?
假設沒有自環,而且兩個頂點間至多隻有一條邊(即沒有平行邊)
Cycle 代碼
public class Cycle { private boolean[] marked; private boolean hasCycle; public Cycle(Graph G) { marked = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) if (!marked[s]) dfs(G, s, s); } private void dfs(Graph G, int v, int u) { // 新增參數u。這個team手把手帶你的師傅被遞歸進去了。。。看上去很簡單,想起來很複雜。。。 marked[v] = true; for (int w : G.adj(v)) if (!marked[w]) dfs(G, w, v); // w是在前線小學徒,v是帶你入行的師傅 else if (w != u) hasCycle = true; //在這裏判斷,這個不等於的判斷是由於w是從u過來的,所以要排除掉這個單向通道。 } public boolean hasCycle() { return hasCycle; } }
可以用兩種顏色將全部頂點着色,使得任意一條邊的兩個頂點顏色不一樣嗎?
這個問題等價於,這是一個二分圖嗎?(什麼叫二分圖?)
TwoColor 代碼
public class TwoColor { private boolean[] marked; private boolean[] color; //由於只有兩色,用boolean就能夠了 private boolean isTwoColorable = true; public TwoColor(Graph G) { marked = new boolean[G.V()]; color = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) if (!marked[s]) dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; for (int w : G.adj(v)) if (!marked[w]) { color[w] = !color[v]; // 沒訪問過的賦個顏色 dfs(G, w); } else if (color[w] == color[v]) isTwoColorable = false; //同色的話 就抱歉了 和環的問題有點像 } public boolean isBipartite() { return isTwoColorable; } }
單點最短路徑
給定一副圖G和一個頂點s,從s到給定頂點v是否存在一條路徑?若是有,找出其中最短(所含邊數最少)的路徑。
五湖四海先識得,泛泛之交後深掘。
先進先出Queue q。
給定頂點s
將v壓入q
大循環:彈出q直到q爲空,當前頂點爲v
小循環:遍歷與v相鄰的全部頂點,當前頂點爲w
判斷w是否已訪問,若是未被訪問
標記w爲已訪問
壓入q
直到大循環q爲空,循環結束。
public class BreadthFirstPaths { private final int s; private boolean[] marked; private int[] edgeTo; BreadthFirstPaths (Graph G, int s) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; this.s = s; bfs(G, s); } public void bfs(Graph G, int s) { Queue<Integer> q = new LinkedList<Integer>(); marked[s] = true; q.offer(s); while (!q.isEmpty()) { int v = q.poll(); for (int w : G.adj(v)) if (!marked[w]) { marked[w] = true; edgeTo[w] = v; q.offer(w); } } } }
待研究,隊列Queue q
q.add(); q.remove()會throw異常
q.offer();q.poll()好一些
StringBuiler類Queue q q.offer() q.poll()