一些關於圖的定義:前端
圖是由一組頂點和一組可以將兩個頂點相連的邊組成。java
連通圖:若是從任意一個頂點都存在一條路徑到達另外一個任意頂點,就稱爲連通圖,一個非連通圖由若干連通的部分組成,都稱爲極大連通子圖。node
無向圖:即鏈接兩個頂點的邊是沒有方向的。數組
無向圖的數據結構:數據結構
使用鄰接表來表示圖:this
如上圖所示,使用一個鏈表數組來表示圖,其中數組的索引表示全部的頂點,每一個數組中存放的鏈表表示全部與此頂點相連的頂點,也能夠理解爲邊。spa
數據結構以下:code
先看鏈表:component
public class LinkedList<T> { private Integer N = 0; private Node<T> root; //鏈表的結點 @SuppressWarnings("hiding") class Node<T> { T value; Node<T> next; public Node(T t,Node<T> node) { this.value = t; this.next = node; } } //添加一個結點 public void add(Node<T> node) { if(root == null) root = node; else { Node<T> temp = root; root = node; root.next = temp; } N++; } public void add(T t) { Node<T> node = new Node<T>(t,null); if(root == null) root = node; else { Node<T> temp = root; root = node; root.next = temp; } N++; } //鏈表的節點數 public Integer size() { return N; } public Node<T> getRoot() { return root; } //遍歷鏈表 public void traverse() { Node<T> node = root; while(node != null) { System.out.print(node.value + " "); node = node.next; } } //獲取鏈表上的全部節點的值,返回一個數組 public Integer[] getAllValue(LinkedList<T> linkedList) { if(linkedList.root == null) return null; Integer[] result = new Integer[linkedList.size()]; Node<T> node = linkedList.root; Integer i = 0; while(node != null) { result[i++] = (Integer)node.value; node = node.next; } return result; } }
圖的數據結構:blog
public class UndiGraphBase { //頂點數目 private Integer V; //邊的數目 private Integer E; //鄰接表 public LinkedList<Integer>[] adj; //建立一個含有V個頂點但不含有邊的圖 @SuppressWarnings("unchecked") void graph(Integer V) { this.V = V; this.E = 0; adj = (LinkedList<Integer>[]) new LinkedList[V]; for(int v=0;v<V;v++) { adj[v] = new LinkedList<Integer>(); } } public Integer V() { return V; } public Integer E() { return E; } //添加一條邊 public void addEdge(Integer v,Integer w) { adj[v].add(w); adj[w].add(v); E++; } //遍歷 public void traverse() { for(int i=0;i<V;i++) { System.out.print(i + ": "); adj[i].traverse(); System.out.println(); } } //獲取某個頂點全部相鄰的頂點 public Integer[] getAllValueByIndex(Integer i) { return adj[i].getAllValue(adj[i]); } }
只要靜下心來看,這些代碼都比較簡單,而且都有註釋,就很少說了。
深度優先遍歷相似於一我的走迷宮:
如圖所示,從起點開始選擇一條邊走到下一個頂點,沒到一個頂點便標記此頂點已到達。
當來到一個標記過的頂點時回退到上一個頂點,再選擇一條沒有到達過的頂點。
當回退到的路口已沒有可走的通道時繼續回退。
先給出一個已經構建好的圖,以後的代碼都創建在這個圖的實例上:
public class Instance { public static UndiGraphBase getInstance() { UndiGraphBase undiGraphBase = new UndiGraphBase(); undiGraphBase.graph(13); undiGraphBase.addEdge(0, 5); undiGraphBase.addEdge(4, 3); undiGraphBase.addEdge(0, 1); undiGraphBase.addEdge(9, 12); undiGraphBase.addEdge(6, 4); undiGraphBase.addEdge(5, 4); undiGraphBase.addEdge(0, 2); undiGraphBase.addEdge(11, 12); undiGraphBase.addEdge(9, 10); undiGraphBase.addEdge(0, 6); undiGraphBase.addEdge(7, 8); undiGraphBase.addEdge(9, 11); undiGraphBase.addEdge(5, 3); return undiGraphBase; } }
該實例表示的圖以下:
深度遍歷的順序以下圖所示:
數組marked表示索引所表明的頂點是否被訪問過
數組edgeTO[]表示到達索引所表明的頂點的上一個頂點,經過對此數組的處理便可獲得路徑,便可以用循環,也能夠用棧,由於知道每一個頂點的上一個頂點,因此能夠獲得路徑
遍歷的代碼以下:
public class DepthFirstPath { //全部頂點組成的數組,若是起點到某頂點可達,則爲true,不然爲false private boolean[] marked; //起點 private final Integer startV; //用數組來表示路徑樹,表示起點到可達頂點的路徑。保存的是到達此頂點的上一個頂點 private Integer[] edgeTo; public DepthFirstPath(UndiGraphBase G,Integer startV) { marked = new boolean[G.V()]; edgeTo = new Integer[G.V()]; this.startV = startV; dfs(G,startV); } public void dfs(UndiGraphBase G,Integer v) { marked[v] = true; Integer[] allNodeValues = G.getAllValueByIndex(v); if(allNodeValues == null) return; for(int i=0;i<allNodeValues.length;i++) { if((!marked[allNodeValues[i]])) { edgeTo[allNodeValues[i]] = v; dfs(G,allNodeValues[i]); } } } //獲取頂點到v的路徑 public void getPath(UndiGraphBase G,Integer v) { if(!hasPathTo(v)) return; //存儲路徑 Stack<Integer> path = new Stack<Integer>(); for(int i=v;i!=startV;i = edgeTo[i]) path.push(i); //加入起點 path.push(startV); //打印棧(即起點startV到v的路徑) System.out.println(path.toString()); } //從起點startV是否有路徑通往v(便是否可達) public boolean hasPathTo(Integer v) { return marked[v]; } public static void main(String[] args) { UndiGraphBase G = Instance.getInstance(); DepthFirstPath depthFirstPath = new DepthFirstPath(G,0); depthFirstPath.getPath(G, 3); } }
其實整個代碼是比較好懂的,主要是edgeto數組須要理解一下。
廣度優先遍歷其實比深度優先遍歷好理解,即從起點開始,遍歷全部與起點連通的頂點,再遍歷與這些頂點連通的頂點,即先搜索距離起點距離爲1的頂點,再遍歷與起點距離爲2的頂點.....
其中經edgeTo數組的意義與深度優先中edgeTo數組同樣。
而distTo[]數組則表示與起點的距離,圖中使用該數組只是爲了便於理解,代碼中能夠不用,由於距離能夠經過遍歷edgeTO[]數組獲得
廣度優先遍歷的代碼:
public class BreadthFirstPath { //到達該頂點的最短路徑是否已知 private boolean[] marked; //到達該頂點的已知路徑上的最後一個頂點 private Integer[] edgeTo; //起點 private final Integer start; public BreadthFirstPath(UndiGraphBase G,Integer start) { marked = new boolean[G.V()]; edgeTo = new Integer[G.V()]; this.start = start; bfs(G,start); } private void bfs(UndiGraphBase G,Integer start) { Queue<Integer> queue = new java.util.LinkedList<Integer>(); //標記起點 marked[start] = true; queue.add(start); while(!queue.isEmpty()) { //從隊列中刪除下一頂點 Integer v = queue.remove(); Integer[] allNodeValues = G.getAllValueByIndex(v); for(int i=0;i<allNodeValues.length;i++) { //對於每一個未被標記的頂點 if(!marked[allNodeValues[i]]) { //保存最短路徑的最後一個頂點 edgeTo[allNodeValues[i]] = v; //標記它,由於最短路徑已知 marked[allNodeValues[i]] = true; queue.add(allNodeValues[i]); } } } } public boolean hasPathTo(Integer v) { return marked[v]; } //獲取頂點到v的路徑 public void getPath(UndiGraphBase G,Integer v) { if(!hasPathTo(v)) return; //存儲路徑 Stack<Integer> path = new Stack<Integer>(); for(int i=v;i!=start;i = edgeTo[i]) path.push(i); //加入起點 path.push(start); //打印棧(即起點startV到v的路徑) System.out.println(path.toString()); } public static void main(String[] args) { UndiGraphBase G = Instance.getInstance(); BreadthFirstPath breadthFirstPath = new BreadthFirstPath(G,0); breadthFirstPath.getPath(G, 3); } }
public class ConnectedComponent { //頂點是否聯通 private boolean[] marked; //聯通份量標識符 private Integer[] id; //聯通份量數目 private Integer count = 0; public ConnectedComponent(UndiGraphBase G) { marked = new boolean[G.V()]; id = new Integer[G.V()]; for(int i=0;i<G.V();i++) { if(!marked[i]) { dfs(G,i); count++; } } } public void dfs(UndiGraphBase G,Integer v) { marked[v] = true; id[v] = count; Integer[] allNodeValues = G.getAllValueByIndex(v); for(int i=0;i<allNodeValues.length;i++) { if((!marked[allNodeValues[i]])) dfs(G,allNodeValues[i]); } } //判斷v和w是否聯通 public boolean connected(Integer v,Integer w) { return id[v] == id[w]; } //連通份量數 public Integer count() { return count; } //v頂點所在的聯通份量的標識符(0-count()-1) public Integer id(Integer v) { return id[v]; } public Integer[] getID() { return id; } public static void main(String[] args) { UndiGraphBase G = Instance.getInstance(); ConnectedComponent cc = new ConnectedComponent(G); Integer m = cc.count(); System.out.println(m + " components"); Integer[] id = cc.getID(); for(int j=0;j<m;j++) { for(int i=0;i<id.length;i++) { if(id[i] == j) { System.out.print(i + " "); } } System.out.println(); } } }
其中marked數組表示索引所表明的頂點是否已經被訪問
Id數組表示索引所表明的頂點屬於哪一個連通份量,假如一個圖總共有n個份量,那麼各個連通份量分別用0,1,2....n-1表示,id數組的值便是該頂點屬於第幾個連通份量。
深度優先遍歷不斷深刻圖中並在棧中保存了全部分叉的頂點;廣度優先遍歷則像扇面通常掃描圖,用一個隊列保存訪問過的最前端的頂點。深度遍歷的方式是尋找離起點更遠的頂點,只在碰到死衚衕時才訪問進出的頂點,廣度遍歷則會首先覆蓋起點附近的頂點,只在臨近的頂點都被訪問完後,纔去訪問更遠的頂點。
深度優先遍歷的路徑一般較長而曲折,廣度優先遍歷的路徑一般短而直接。
關於廣度優先遍歷和深度優先遍歷的使用場景與效率以下圖所示: