圖是一種比線性表和樹更復雜的數據結構,在圖中,結點之間的關係是任意的,任意兩個數據元素之間均可能相關。圖是一種多對多的數據結構。html
圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,一般表示爲:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。java
注意:線性表中能夠沒有元素,稱爲空表。樹中能夠沒有結點,叫作空樹。可是在圖中不容許沒有頂點,能夠沒有邊。node
基本術語:算法
無向邊:若頂點Vi和Vj之間的邊沒有方向,稱這條邊爲無向邊(Edge),用(Vi,Vj)
來表示。數組
無向圖(Undirected graphs):圖中任意兩個頂點的邊都是無向邊。數據結構
有向邊:若從頂點Vi到Vj的邊有方向,稱這條邊爲有向邊,也稱爲弧(Arc),用<Vi, Vj>
來表示,其中Vi稱爲弧尾(Tail),Vj稱爲弧頭(Head)。less
有向圖(Directed graphs):圖中任意兩個頂點的邊都是有向邊。ide
簡單圖:不存在自環(頂點到其自身的邊)和重邊(徹底相同的邊)的圖測試
無向徹底圖:無向圖中,任意兩個頂點之間都存在邊。this
有向徹底圖:有向圖中,任意兩個頂點之間都存在方向相反的兩條弧。
稀疏圖;有不多條邊或弧的圖稱爲稀疏圖,反之稱爲稠密圖。
權(Weight):表示從圖中一個頂點到另外一個頂點的距離或耗費。
網:帶有權重的圖
度:與特定頂點相鏈接的邊數;
出度、入度:有向圖中的概念,出度表示以此頂點爲起點的邊的數目,入度表示以此頂點爲終點的邊的數目;
環:第一個頂點和最後一個頂點相同的路徑;
簡單環:除去第一個頂點和最後一個頂點後沒有重複頂點的環;
連通圖:任意兩個頂點都相互連通的圖;
極大連通子圖:包含竟可能多的頂點(必須是連通的),即找不到另一個頂點,使得此頂點可以鏈接到此極大連通子圖的任意一個頂點;
連通份量:極大連通子圖的數量;
強連通圖:此爲有向圖的概念,表示任意兩個頂點a,b,使得a可以鏈接到b,b也能鏈接到a 的圖;
生成樹:n個頂點,n-1條邊,而且保證n個頂點相互連通(不存在環);
最小生成樹:今生成樹的邊的權重之和是全部生成樹中最小的;
AOV網(
Activity On Vertex Network ):在有向圖中若以頂點表示活動,有向邊表示活動之間的前後關係
AOE網(
Activity On Edge Network):在帶權有向圖中若以頂點表示事件,有向邊表示活動,邊上的權值表示該活動持續的時間
因爲圖的結構比較複雜,任意兩個頂點之間均可能存在關係,所以用簡單的順序存儲來表示圖是不可能,而若使用多重鏈表的方式(即一個數據域多個指針域的結點來表示),這將會出現嚴重的空間浪費或操做不便。這裏總結一下經常使用的表示圖的方法:
圖的鄰接矩陣(Adjacency Matrix)存儲方式是用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱鄰接矩陣)存儲圖中的邊或弧的信息。
無向圖因爲邊不區分方向,因此其鄰接矩陣是一個對稱矩陣。鄰接矩陣中的0表示邊不存在,主對角線全爲0表示圖中不存在自環。
帶權有向圖的鄰接矩陣:
在帶權有向圖的鄰接矩陣中,數字表示權值weight,「無窮」表示弧不存在。因爲權值可能爲0,因此不能像在無向圖的鄰接矩陣中那樣使用0來表示弧不存在。
代碼:
/** * 有向圖的鄰接矩陣實現 */ public class Digraph { private int vertexsNum; private int edgesNum; private int[][] arc; public Digraph(int[][] data, int vertexsNum) { this.vertexsNum = vertexsNum; this.edgesNum = data.length; arc = new int[vertexsNum][vertexsNum]; for (int i = 0; i < vertexsNum; i++) { for (int j = 0; j < vertexsNum; j++) { arc[i][j] = Integer.MAX_VALUE; } } for (int i = 0; i < data.length; i++) { int tail = data[i][0]; int head = data[i][1]; arc[tail][head] = 1; } } //用於測試,返回一個頂點的鄰接點 public Iterable<Integer> adj(int vertex) { Set<Integer> set = new HashSet<>(); for (int i = 0; i < vertexsNum; i++) { if (arc[vertex][i] != Integer.MAX_VALUE) set.add(i); } return set; } public static void main(String[] args) { int[][] data = { {0,3}, {1,0}, {1,2}, {2,0}, {2,1}, }; Digraph wd = new Digraph(data,4); for(int i :wd.adj(1)) { System.out.println(i); } } }
優缺點:
鄰接表是一種將數組與鏈表相結合的存儲方法。其具體實現爲:將圖中頂點用一個一維數組存儲,每一個頂點Vi的全部鄰接點用一個單鏈表來存儲。這種方式和樹結構中孩子表示法同樣。
對於有向圖其鄰接表結構以下:
有向圖的鄰接表是以頂點爲弧尾來存儲邊表的,這樣很容易求一個頂點的出度(頂點對應單鏈表的長度),但若求一個頂點的入度,則需遍歷整個圖才行。這時能夠創建一個有向圖的逆鄰接表即對每一個頂點v都創建一個弧頭尾v的單鏈表。如上圖所示。
代碼:
/** * 有向圖的鄰接表實現 * */ public class AdjListDigraph { private class EdgeNode { int index; EdgeNode next; EdgeNode(int index, EdgeNode next){ this.index = index; this.next = next; } } private class VertexNode { int id; EdgeNode headNode; } private VertexNode[] vertexs; private int vertexsNum; private int edgesNum; public AdjListDigraph(int[][] data, int vertexsNum) { this.vertexsNum = vertexsNum; this.edgesNum = data.length; vertexs = new VertexNode[vertexsNum]; for (int i = 0; i < vertexs.length; i++) { vertexs[i] = new VertexNode(); vertexs[i].id = i; // } for (int i = 0; i < data.length; i++) { int index = data[i][1]; EdgeNode next = vertexs[data[i][0]].headNode; EdgeNode eNode = new EdgeNode(index,next); vertexs[data[i][0]].headNode = eNode; //頭插法 } } //用於測試,返回一個頂點的鄰接點 public Iterable<Integer> adj(int index) { Set<Integer> set = new HashSet<>(); EdgeNode current = vertexs[index].headNode; while(current != null) { VertexNode node = vertexs[current.index]; set.add(node.id); current = current.next; } return set; } public static void main(String[] args) { int[][] data = { {0,3}, {1,0}, {1,2}, {2,0}, {2,1}, }; AdjListDigraph ald = new AdjListDigraph(data,4); for(int i :ald.adj(1)) { System.out.println(i); } } }
本算法的時間複雜度爲 O(N + E),其中N、E分別爲頂點數和邊數,鄰接表實現比較適合表示稀疏圖。
十字鏈表(Orthogonal List)是將鄰接表和逆鄰接表相結合的存儲方法,它解決了鄰接表(或逆鄰接表)的缺陷,即求入度(或出度)時必須遍歷整個圖。
十字鏈表的結構以下:
圖中:
代碼實現:
/** * 有向圖的十字鏈表實現 * */ public class OrthogonalList { private class EdgeNode { int tailVex; int headVex; EdgeNode headNext; EdgeNode tailNext; public EdgeNode(int tailVex, int headVex, EdgeNode headNext, EdgeNode tailNext) { super(); this.tailVex = tailVex; this.headVex = headVex; this.headNext = headNext; this.tailNext = tailNext; } } private class VertexNode { int data; EdgeNode firstIn; EdgeNode firstOut; } private VertexNode[] vertexs; private int vertexsNum; private int edgesNum; public OrthogonalList(int[][] data, int vertexsNum) { this.vertexsNum = vertexsNum; this.edgesNum = data.length; vertexs = new VertexNode[vertexsNum]; for (int i = 0; i < vertexs.length; i++) { vertexs[i] = new VertexNode(); vertexs[i].data = i; // } //關鍵 for (int i = 0; i < data.length; i++) { int tail = data[i][0]; int head = data[i][1]; EdgeNode out = vertexs[tail].firstOut; EdgeNode in = vertexs[head].firstIn; EdgeNode eNode = new EdgeNode(tail,head,in,out); vertexs[tail].firstOut = eNode; vertexs[head].firstIn = eNode; } } //返回一個頂點的出度 public int outDegree(int index) { int result = 0; EdgeNode current = vertexs[index].firstOut; while(current != null) { current = current.tailNext; result++; } return result; } //返回一個頂點的入度 public int inDegree(int index) { int result = 0; EdgeNode current = vertexs[index].firstIn; while(current != null) { current = current.headNext; result++; } return result; } public static void main(String[] args) { int[][] data = { {0,3}, {1,0}, {1,2}, {2,0}, {2,1}, }; OrthogonalList orth = new OrthogonalList(data,4); System.out.println("頂點1的出度爲" + orth.outDegree(1)); System.out.println("頂點1的入度爲" + orth.inDegree(1)); } }
十字鏈表建立圖算法的時間複雜度和鄰接表相同都爲O(N + E)。在有圖的應用中推薦使用。
從圖的某個頂點出發,遍歷圖中其他頂點,且使每一個頂點僅被訪問一次,這個過程叫作圖的遍歷(Traversing Graph)。對於圖的遍歷一般有兩種方法:深度優先遍歷和廣度優先遍歷。
深度優先遍歷(Depth First Search,簡稱DFS),也成爲深度優先搜索。
遍歷思想:基本思想:首先從圖中某個頂點v0出發,訪問此頂點,而後依次從v相鄰的頂點出發深度優先遍歷,直至圖中全部與v路徑相通的頂點都被訪問了;若此時尚有頂點未被訪問,則從中選一個頂點做爲起始點,重複上述過程,直到全部的頂點都被訪問。
深度優先遍歷用遞歸實現比較簡單,只需用一個遞歸方法來遍歷全部頂點,在訪問某一個頂點時:
深度優先遍歷的過程:
代碼以下:
public class DFSTraverse { private boolean[] visited; //從頂點index開始遍歷 public DFSTraverse(Digraph graph, int index) { visited = new boolean[graph.getVertexsNum()]; dfs(graph,index); } private void dfs(Digraph graph, int index) { visited[index] = true; for(int i : graph.adj(index)) { if(!visited[i]) dfs(graph,i); } } }
廣度優先遍歷(Breadth First Search,簡稱BFS),又稱爲廣度優先搜索
遍歷思想:首先,從圖的某個頂點v0出發,訪問了v0以後,依次訪問與v0相鄰的未被訪問的頂點,而後分別從這些頂點出發,廣度優先遍歷,直至全部的頂點都被訪問完。
廣度優先遍歷的過程:
代碼:
public class BFSTraverse { private boolean[] visited; public BFSTraverse(AdjListDigraph graph, int index) { visited = new boolean[graph.getVertexsNum()]; bfs(graph,index); } private void bfs(AdjListDigraph graph, int index) { //在JSE中LinkedList實現了Queue接口 Queue<Integer> queue = new LinkedList<>(); visited[index] = true; queue.add(index); while(!queue.isEmpty()) { int vertex = queue.poll(); for(int i : graph.adj(vertex)) { if(!visited[i]) { visited[i] = true; queue.offer(i); } } } } }
圖的生成樹是它的一棵含有全部頂點的無環連通子圖。一棵加權圖的最小生成樹(MST)是它的一棵權值(全部邊的權值之和)最小的生成樹。
計算最小生成樹可能遇到的狀況:
圖的切分是將圖的全部頂點分爲兩個非空且不重疊的兩個集合。橫切邊是一條鏈接兩個屬於不一樣集合的頂點的邊。
切分定理:在一幅加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於圖的最小生成樹。
切分定理是解決最小生成樹問題的全部算法的基礎。這些算法都是貪心算法。
首先先構造一個帶權的無向圖,其代碼以下:
//定義邊 public class Edge implements Comparable<Edge>{ private final int ver1; private final int ver2; private final Integer weight; public Edge(int ver1, int ver2, int weight) { super(); this.ver1 = ver1; this.ver2 = ver2; this.weight = weight; } //返回一個頂點 public int either() { return ver1; } //返回另外一個頂點 public int other(int vertex) { if (vertex == ver1) return ver2; else if(vertex == ver2) return ver1; else throw new RuntimeException("邊不一致"); } @Override public int compareTo(Edge e) { return this.weight.compareTo(e.weight); } public Integer getWeight() { return weight; } @Override public String toString() { return "Edge [" + ver1 + "," + ver2 +"]"; } } /** * 帶權無向圖的實現 */ public class WeightedGraph { private final int vertexsNum; private final int edgesNum; private List<Edge>[] adj; public WeightedGraph(int[][] data, int vertexsNum) { this.vertexsNum = vertexsNum; this.edgesNum = data.length; adj = (List<Edge>[]) new ArrayList[vertexsNum]; for(int i=0; i<vertexsNum; i++) { adj[i] = new ArrayList<>(); } for (int i = 0; i < data.length; i++) { Edge edge = new Edge(data[i][0],data[i][1],data[i][2]); int v = edge.either(); adj[v].add(edge); adj[edge.other(v)].add(edge); } } public Iterable<Edge> adj(int vertex) { return adj[vertex]; } public int getVertexsNum() { return vertexsNum; } public int getEdgesNum() { return edgesNum; } public Iterable<Edge> getEdges() { List<Edge> edges = new ArrayList<>(); for(int i=0; i<vertexsNum; i++) { for(Edge e : adj[i]) { if(i > e.other(i)) { //無向圖,防止將一條邊加入兩次 edges.add(e); } } } return edges; } }
每次將權值最小的橫切邊加入生成樹中
1)、Prim算法的延遲實現
實現過程以下圖:
從頂點0開始,首先將頂點0加入到樹中(標記),頂點0和其它點的橫切邊(這裏即爲頂點0的鄰接邊)加入優先隊列,將權值最小的橫切邊出隊,加入生成樹中。此時至關於也向樹中添加了一個頂點2,接着將集合(頂點1,2組成)和另外一個集合(除1,2的頂點組成)間的橫切邊加入到優先隊列中,如此這般,直到隊列爲空。
注意:若橫切邊中另外一個頂點在樹中,則此邊失效。
代碼以下:
public class LazyPrimMST { private boolean[] visited; //標記頂點 private LinkedQueue<Edge> mst; //存儲最小生成樹的邊 private MinPQ<Edge> pq; //優先隊列,權值越最小優先級越高 public LazyPrimMST(WeightedGraph wg) { visited = new boolean[wg.getVertexsNum()]; mst = new LinkedQueue<Edge>(); pq = new MinPQ<>(wg.getVertexsNum()); visit(wg, 0); //從0點開始 while(!pq.isEmpty()) { Edge e = pq.deQueue(); int ver1 = e.either(); int ver2 = e.other(ver1); if(visited[ver1] && visited[ver2]) { continue; //邊失效 } mst.enQueue(e); if(!visited[ver1]) visit(wg, ver1); if(!visited[ver2]) visit(wg, ver2); } } private void visit(WeightedGraph wg, int ver) { visited[ver] = true; //標記頂點 for(Edge e : wg.adj(ver)) { if(!visited[e.other(ver)]) pq.enQueue(e); } } public Iterable<Edge> getMST() { return mst; } public static void main(String[] args) { int[][] data = { {0, 2, 2}, {0, 1, 4}, {0, 5, 5}, {1, 2, 3}, {1, 5, 11}, {1, 3, 7}, {2, 3, 8}, {2, 4, 10}, {3, 5, 6}, {3, 4, 1}, {4, 5, 9} }; WeightedGraph wg = new WeightedGraph(data,6); LazyPrimMST lpm = new LazyPrimMST(wg); for(Edge e : lpm.getMST()) { System.out.println(e); } } }
其中,LinkedQueue類的代碼在《數據結構與算法(三),棧與隊列》中;而MinPQ類的代碼與《數據結構與算法(五),優先隊列》中MaxPQ類的代碼幾乎同樣,只需將方法less中的小於號改成大於號便可。這裏就不在給出代碼了
此方法的時間複雜度爲 O(ElogE),空間複雜度爲 O(E)。其中,V爲頂點個數,E爲邊數
2)、Prim算法即時實現
基於Prim算法的延遲實現,咱們能夠在優先隊列中只保存每一個非樹頂點V的一條邊(即它與樹中的頂點鏈接起來的權重最小的那條邊),由於其餘權重較大的邊早晚都會失效。
實現過程以下圖:
代碼實現:
/** * prim的即時實現 */ public class PrimMST { private Edge[] edgeTo; //點離生成樹最近的邊 private int[] distTo; //點到生成樹的距離 private boolean[] visited; private IndexMinPQ<Integer> pq; //索引優先隊列,關聯頂點與distTo public PrimMST(WeightedGraph wg) { //初始化 edgeTo = new Edge[wg.getVertexsNum()]; distTo = new int[wg.getVertexsNum()]; visited = new boolean[wg.getVertexsNum()]; for(int i=0; i<wg.getVertexsNum(); i++) { distTo[i] = Integer.MAX_VALUE; } pq = new IndexMinPQ<>(wg.getVertexsNum()); distTo[0] = 0; pq.insert(0, 0); while(!pq.isEmpty()) { visit(wg, pq.delMin()); } } private void visit(WeightedGraph wg, int ver) { visited[ver] = true; for(Edge e : wg.adj(ver)) { int vertex = e.other(ver); //邊的另外一個點 if(visited[vertex]) continue; if(e.getWeight() < distTo[vertex]) { edgeTo[vertex] = e; //被覆蓋的邊失效 distTo[vertex] = e.getWeight(); if(pq.contains(vertex)) { pq.change(vertex, distTo[vertex]); }else { pq.insert(vertex, distTo[vertex]); } } } } public Iterable<Edge> getMST() { return Arrays.asList(edgeTo); } }
此方法的時間複雜度爲 O(ElogV),空間複雜度爲 O(V)。其中,V爲頂點個數,E爲邊數。
能夠看出Prim算法的即時實現比延遲實現明顯要快,特別是對於稠密矩陣(E>>>V)的狀況。
Kruskal算法的思想是按照邊的權重順序來生成最小生成樹,首先將圖中全部邊加入優先隊列,將權重最小的邊出隊加入最小生成樹,保證加入的邊不與已經加入的邊造成環,直到樹中有V-1到邊爲止。
實現過程以下圖:
/** * Kruskal算法的實現 */ public class KruskalMST { private List<Edge> mst; //存儲最小生成樹的邊 private MinPQ<Edge> pq; //優先隊列 private int[] parent; //用來判斷邊與邊是否造成迴路 public KruskalMST(WeightedGraph wg) { mst = new ArrayList<Edge>(); pq = new MinPQ<>(wg.getEdgesNum()); parent = new int[wg.getVertexsNum()]; for(Edge e : wg.getEdges()) { pq.enQueue(e); } //最小生成樹的邊最多爲V-1個 while(!pq.isEmpty() && mst.size() < wg.getVertexsNum() - 1) { Edge e = pq.deQueue(); int v = e.either(); int n = find(parent, v); int m = find(parent, e.other(v)); if(n != m) { //表示此邊沒有與生成樹造成環路 parent[n] = m; mst.add(e); } } } //查找鏈接樹的尾部下標 private int find(int[] data, int v) { while(parent[v] > 0) { v = parent[v]; } return v; } public Iterable<Edge> getMST() { return mst; } }
Kruskal算法的時間複雜度最壞狀況下爲O(ElogE)。空間複雜度爲O(E)。
對比Prim算法和Kruskal算法,Kruskal算法主要根據邊來生成樹,邊數少時效率比較高,適合稀疏圖;而Prim算法對邊數多的稠密圖效果更好一些。
最短路徑指兩頂點之間通過的邊上權值之和最少的路徑,而且稱路徑上的第一個頂點爲源點,最後一個頂點爲終點。
爲了操做方便,首先使用面向對象的方法,來實現一個加權的有向圖,其代碼以下:
/** * 有向邊 */ public class Edge{ private final int from; private final int to; private final int weight; public Edge(int from, int to, int weight) { super(); this.from = from; this.to = to; this.weight = weight; } public int getFrom() { return from; } public int getTo() { return to; } public int getWeight() { return weight; } } //帶權有向圖的實現 public class WeightedDigraph { private final int vertexsNum; private final int edgesNum; private List<Edge>[] adj; //鄰接表 public WeightedDigraph(int[][] data, int vertexsNum) { this.vertexsNum = vertexsNum; this.edgesNum = data.length; adj = (List<Edge>[]) new ArrayList[vertexsNum]; for(int i=0; i<vertexsNum; i++) { adj[i] = new ArrayList<>(); } for (int i = 0; i < data.length; i++) { Edge edge = new Edge(data[i][0],data[i][1],data[i][2]); int v = edge.getFrom(); adj[v].add(edge); } } public Iterable<Edge> adj(int vertex) { return adj[vertex]; } public int getVertexsNum() { return vertexsNum; } public int getEdgesNum() { return edgesNum; } //有向圖中全部的邊 public Iterable<Edge> getEdges() { List<Edge> edges = new ArrayList<>(); for(List<Edge> list : adj) { for(Edge e : list) { edges.add(e); } } return edges; } }
頂點到源點s的最短路徑,咱們使用一個用頂點索引的Edge數組(edgeTo[])來存儲,使用數組distTo[]來存儲最短路徑樹(包含了源點S到全部可達頂點的最短路徑)。
邊的鬆弛操做:
邊的鬆弛過程以下圖:
鬆弛邊【1,4】就是檢查頂點0到4的最短路徑是不是先從頂點0到1,而後在由頂點1到4。若是是則【0,4】邊失效,將【1,4】加入最短路徑樹。
代碼:
private void relax(WeightedDigraph wd,Edge e) { int v = e.getFrom(); int w = e.getTo(); if(distTo[w] > distTo[v] + e.getWeight()) { distTo[w] = distTo[v] + e.getWeight(); edgeTo[w] = e; } }
頂點的鬆弛操做:
頂點的鬆弛就是鬆弛頂點的全部鄰接邊,這裏就不給出過程了,實現代碼在Dijkstra實現中。
算的的實現過程:
Dijkstra算法的代碼實現:
//Dijkstra算法的實現 public class Dijkstra { private Edge[] edgeTo; //最短路徑樹 private int[] distTo; //存儲每一個頂點到源點的距離 //索引優先隊列,創建distTo和頂點索引,distTo越小,優先級越高 private IndexMinPQ<Integer> pq; public Dijkstra(WeightedDigraph wd, int s) { edgeTo = new Edge[wd.getVertexsNum()]; distTo = new int[wd.getVertexsNum()]; pq = new IndexMinPQ<>(wd.getVertexsNum()); for(int i=0; i<wd.getVertexsNum(); i++) { distTo[i] = Integer.MAX_VALUE; } distTo[s] = 0; //源點s的distTo爲0 pq.insert(s, 0); while(pq.isEmpty()) { relax(wd, pq.delMin()); } } //頂點的鬆弛 private void relax(WeightedDigraph wd, int ver) { for(Edge e : wd.adj(ver)) { int v = e.getTo(); if(distTo[v] > distTo[ver] + e.getWeight()) { distTo[v] = distTo[ver] + e.getWeight(); edgeTo[v] = e; if(pq.contains(v)) { pq.change(v, distTo[v]); }else { pq.insert(v, distTo[v]); } } } } }
Dijkstra算法的侷限性:圖中邊的權重必須爲正,但能夠是有環圖。時間複雜度爲O(ElogV),空間複雜度O(V)。
這篇文章寫了很久,陸陸續續差很少快10天了,至今還有如下內容沒有總結:
圖的知識實在是太多了,就先總結到這裏吧,有時間在寫。