@java
圖是一種比線性表和樹更爲複雜的數據結構。在線性表中,數據元素之間僅有線性關係,每一個數據元素只有一個直接前驅和一個直接後繼;在樹形結構中,數據元素之間有着明顯的層次關係,而且每一層中的數據元素可能和下一層中的多個元素(即其孩子結點)相關,但只能和上一層中一個元素(即其雙親結點)相關; 而在圖結構中,結點之間的關係能夠是任意的,圖中任意兩個數據元素之間均可能相關。node
在計算機科學中的圖是由點和邊構成的。git
圖(Graph) G由兩個集合V和E組成,記爲G=(V,E) , 其中V是頂點的有窮非空集合,E是V中頂點偶對的有窮集合,這些頂點偶對稱爲邊。V(G)和E(G)一般分別表示圖G的頂點集合和邊集合,E(G)能夠爲空集。若 E(G)爲空,則圖G只有頂點而沒有邊。github
對於圖G ,若邊集E(G)爲有向邊的集合,則稱該圖爲有向圖;若邊集E(G)爲無向邊的集合,則稱該圖爲無向圖。算法
在有向圖中,頂點對<x, y>是有序的,它稱爲從頂點 x到頂點y的一條有向邊。 所以<x,y>與<y, x>是不一樣的兩條邊。 頂點對用一對尖括號括起來,x是有向邊的始點,y是有向邊的終點。<x, y>也稱做一條弧,則 x爲弧尾, y爲弧頭。segmentfault
在無向圖中,頂點對<x, y>是無序的,它稱爲從頂點 x與頂點y相關聯的一條邊。這條邊沒有特定的方向,(x,y) 和 (y,x)是同一條邊。爲了區別於有向圖,無向圖的一對頂點用括號括起來。數組
用n表示圖中頂點數目,用e表示邊的數目, 來看看圖結構中的一些基本術語。數據結構
無向徹底圖和有向徹底圖:對千無向圖, 若具備 n(n- 1)/2 條邊,則稱爲無向徹底圖。對於有向圖, 若具備 n(n- l)條弧,則稱爲有向徹底圖。ide
稀疏圖和稠密圖:有不多條邊或弧(如 e<nlog2n) 的圖稱爲稀疏圖, 反之稱爲稠密圖。學習
權和網:在實際應用中,每條邊能夠標上具備某種含義的數值,該數值稱爲該邊上的權。這些權能夠表示從一個頂點到另外一個頂點的距離或耗費。這種帶權的圖一般稱爲網。
鄰接點:對於 無向圖 G, 若是圖的邊 (v, v')\(\in\)E, 則稱頂點 v 和 v'互爲鄰接點, 即 v 和 v'相鄰接。邊 (v, v')依附於頂點 v 和 v', 或者說邊 (v, v')與頂點 v 和 v'相關聯。
度、入度和出度:頂知的度是指和v 相關聯的邊的數目,記爲 TD(v) 。例如,圖2 (b) 中G2的頂點 V3 的度是3。對於有向圖,頂點v的度分爲入度和出度。入度是以頂點v爲頭的弧的數目,記爲 ID(v); 出度是以頂點 v 爲尾的弧的數目,記爲OD(v)。頂點 v 的度爲 TD(v) = ID(v) + OD(可。例如,圖2中 G1 的頂點v1的入度 ID(v1)=1, 出度 OD(v1)=2, 度TD(v1)= ID(v1) + OD(v1) =3。通常地,若是頂點 Vi 的度記爲 TD(vi),那麼一個有n個頂點,e條邊的圖,知足以下關係:
路徑和路徑長度:在無向圖 G 中,從 頂點 v 到頂點 v'的 路徑是一個頂點序列 (v = vi,0,Vi, 1,…, i;, m= v'), 其中 (vi,j-1, vi,j)\(\in\)E, 其中1\(\leq\)j\(\leq\)m。 若是 G 是有向圖, 則路徑也是有向的,頂點序列應滿 足 <v;,1-1, vi,j)>\(\in\)E, 其中1\(\leq\)j\(\leq\)m。 路徑長度是一條路徑上通過的邊或弧的數目。
迴路或環:第一個頂點和最後一個頂點相同的路徑稱爲迴路或環。
簡單路徑、 簡單迴路或簡單環:序列中頂點不重複出現的路徑稱爲簡單路徑。除了第一個頂點和最後一個頂點以外, 其他頂點不重複出現的迴路,稱爲簡單迴路或簡單環。
連通、連通圖和連通份量:在無向圖 G 中,若是從頂點 v 到頂點 v'有路徑,則稱 v 和 v'是連通的。若是對於圖中任意兩個頂點 Vi、 Vj\(\in\)V, Vi 和 Vj 都是連通的,則稱 G 是連通圖。圖 2
(b)中的 G2 就是一個連通圖,而圖 4 (a) 中的 G3 則是非連通圖,但 G3 有 3個連通份量,如圖
4 (b) 所示。所謂連通份量, 指的是無向圖中的極大連通子圖。
圖的存儲結構相較線性表與樹來講就更加複雜。
圖的存儲結構比較常見的有兩種,鄰接矩陣和鄰接表。
具體地,若圖 G 中包含 n 個頂點,咱們就使用一個 n×n 的方陣 A,並使每一頂點都分別對應於某一行(列)。既然圖所描述的是這些頂點各自對應的元素之間的二元關係,故能夠很天然地將任意一對元素 u 和 v 之間可能存在二元關係與矩陣 A 中對應的單元 A[u, v]對應起來: 1 或 true 表示存在關係, 0 或 false 表示不存在關係。這一矩陣中的各個單元分別描述了一對元素之間可能存在的鄰接關係,故此得名。
(a)是無向圖, (b)是有向圖。無向圖的鄰接矩陣,是一個對稱矩陣。在圖中所示的矩陣,a[i][j] 值都爲1,若是是帶權的圖,咱們能夠將其設置爲權值。
這一表示形式也能夠推廣至帶權圖,具體方法是,將每條邊的權重記錄在該邊對應得矩陣單元中。
須要注意的是:
圖的鄰接矩陣表示方法簡單實現以下:
/** * @Author 三分惡 * @Date 2020/11/28 * @Description 圖的鄰接矩陣存儲實現 */ public class AMWGraph { private ArrayList vertexList;//存儲點的鏈表 private int[][] edges;//鄰接矩陣,用來存儲邊 private int numOfEdges;//邊的數目 public AMWGraph(int n) { //初始化矩陣,一維數組,和邊的數目 edges=new int[n][n]; vertexList=new ArrayList(n); numOfEdges=0; } //獲得結點的個數 public int getNumOfVertex() { return vertexList.size(); } //獲得邊的數目 public int getNumOfEdges() { return numOfEdges; } //返回結點i的數據 public Object getValueByIndex(int i) { return vertexList.get(i); } //返回v1,v2的權值 public int getWeight(int v1,int v2) { return edges[v1][v2]; } //插入結點 public void insertVertex(Object vertex) { vertexList.add(vertexList.size(),vertex); } //插入結點 public void insertEdge(int v1,int v2,int weight) { edges[v1][v2]=weight; numOfEdges++; } //刪除結點 public void deleteEdge(int v1,int v2) { edges[v1][v2]=0; numOfEdges--; } //獲得第一個鄰接結點的下標 public int getFirstNeighbor(int index) { for(int j=0;j<vertexList.size();j++) { if (edges[index][j]>0) { return j; } } return -1; } //根據前一個鄰接結點的下標來取得下一個鄰接結點 public int getNextNeighbor(int v1,int v2) { for (int j=v2+1;j<vertexList.size();j++) { if (edges[v1][j]>0) { return j; } } return -1; } }
鄰接矩陣雖然比較直觀,可是空間利用率是上並不理想。其中大量的單元所對應的邊有可能並未在圖中出現,這也是靜態向量結構廣泛的不足。既然如此,咱們爲何不將向量改成列表呢?
鄰接表是圖的一種連接存儲結構。 鄰接表表示法只關心存在的邊,將頂點的鄰接邊用列表表示。
咱們來看一下具體的實現。
這是有向圖的抽象接口定義。
/** * @Author 三分惡 * @Date 2020/11/28 * @Description 有向圖接口 */ public interface IDirectGraph<V> { /** * 新增頂點 * * @param v 頂點 * @since 0.0.2 */ void addVertex(final V v); /** * 刪除頂點 * * @param v 頂點 * @return 是否刪除成功 * @since 0.0.2 */ boolean removeVertex(final V v); /** * 獲取頂點 * * @param index 下標 * @return 返回頂點信息 * @since 0.0.2 */ V getVertex(final int index); /** * 新增邊 * * @param edge 邊 * @since 0.0.2 */ void addEdge(final Edge<V> edge); /** * 移除邊 * * @param edge 邊信息 * @since 0.0.2 */ boolean removeEdge(final Edge<V> edge); /** * 獲取邊信息 * * @param from 開始節點 * @param to 結束節點 * @since 0.0.2 */ Edge<V> getEdge(final int from, final int to); }
這是有向圖的邊的實現:
/** * @Author 三分惡 * @Date 2020/11/28 * @Description 邊 */ public class Edge<V> { /** * 開始節點 * @since 0.0.2 */ private V from; /** * 結束節點 * @since 0.0.2 */ private V to; /** * 權重 * @since 0.0.2 */ private double weight; public Edge(V from, V to) { this.from = from; this.to = to; } public V getFrom() { return from; } public void setFrom(V from) { this.from = from; } public V getTo() { return to; } public void setTo(V to) { this.to = to; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } @Override public String toString() { return "Edge{" + "from=" + from + ", to=" + to + ", weight=" + weight + '}'; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Edge<?> edge = (Edge<?>) o; return Double.compare(edge.weight, weight) == 0 && to.equals(edge.to) && from.equals(edge.from); } @Override public int hashCode() { return hashCode(); } }
這裏咱們再也不單獨作頂點的實現,因此 節點=頂點+邊。
/** * @Author 三分惡 * @Date 2020/11/28 * @Description */ public class GraphNode<V> { /** * 頂點信息 * @since 0.0.2 */ private V vertex; /** * 以此頂點爲起點的邊的集合,是一個列表,列表的每一項是一條邊 * * (1)使用集合,避免重複 */ private Set<Edge<V>> edgeSet; /** * 初始化一個節點 * @param vertex 頂點 */ public GraphNode(V vertex) { this.vertex = vertex; this.edgeSet = new HashSet<Edge<V>>(); } /** * 新增一條邊 * @param edge 邊 */ public void add(final Edge<V> edge) { edgeSet.add(edge); } /** * 獲取目標邊 * @param to 目標邊 * @return 邊 * @since 0.0.2 */ public Edge<V> get(final V to) { for(Edge<V> edge : edgeSet) { V dest = edge.getTo(); if(dest.equals(to)) { return edge; } } return null; } /** * 獲取目標邊 * @param to 目標邊 * @return 邊 * @since 0.0.2 */ public Edge<V> remove(final V to) { Iterator<Edge<V>> edgeIterable = edgeSet.iterator(); while (edgeIterable.hasNext()) { Edge<V> next = edgeIterable.next(); if(to.equals(next.getTo())) { edgeIterable.remove(); return next; } } return null; } public V getVertex() { return vertex; } public Set<Edge<V>> getEdgeSet() { return edgeSet; } @Override public String toString() { return "GraphNode{" + "vertex=" + vertex + ", edgeSet=" + edgeSet + '}'; } }
接下來是有向圖的鄰接表表示具體實現。
/** * @Author 三分惡 * @Date 2020/11/28 * @Description */ public class ListDirectGraph<V> implements IDirectGraph<V> { /** * 節點鏈表 * * @since 0.0.2 */ private List<GraphNode<V>> nodeList; /** * 初始化有向圖 * * @since 0.0.2 */ public ListDirectGraph() { this.nodeList = new ArrayList<GraphNode<V>>(); } public void addVertex(V v) { GraphNode<V> node = new GraphNode<V>(v); // 直接加入到集合中 this.nodeList.add(node); } public boolean removeVertex(V v) { //1. 移除一個頂點 //2. 全部和這個頂點關聯的邊也要被移除 Iterator<GraphNode<V>> iterator = nodeList.iterator(); while (iterator.hasNext()) { GraphNode<V> graphNode = iterator.next(); if (v.equals(graphNode.getVertex())) { iterator.remove(); } } return true; } public V getVertex(int index) { return nodeList.get(index).getVertex(); } public void addEdge(Edge<V> edge) { //1. 新增一條邊,直接遍歷列表。 // 若是存在這條的起始節點,則將這條邊加入。 // 若是不存在,則直接報錯便可。 for (GraphNode<V> graphNode : nodeList) { V from = edge.getFrom(); V vertex = graphNode.getVertex(); // 起始節點在開頭 if (from.equals(vertex)) { graphNode.getEdgeSet().add(edge); } } } public boolean removeEdge(Edge<V> edge) { // 直接從列表中對應的節點,移除便可 GraphNode<V> node = getGraphNode(edge); if (null != node) { // 移除目標爲 to 的邊 node.remove(edge.getTo()); } return true; } public Edge<V> getEdge(int from, int to) { // 獲取開始和結束的頂點 V toVertex = getVertex(from); // 獲取節點 GraphNode<V> fromNode = nodeList.get(from); // 獲取對應結束頂點的邊 return fromNode.get(toVertex); } /** * 獲取圖節點 * * @param edge 邊 * @return 圖節點 */ private GraphNode<V> getGraphNode(final Edge<V> edge) { for (GraphNode<V> node : nodeList) { final V from = edge.getFrom(); if (node.getVertex().equals(from)) { return node; } } return null; } /** * 獲取對應的圖節點 * * @param vertex 頂點 * @return 圖節點 * @since 0.0.2 */ private GraphNode<V> getGraphNode(final V vertex) { for (GraphNode<V> node : nodeList) { if (vertex.equals(node.getVertex())) { return node; } } return null; } }
和樹的遍歷相似,圖的遍歷也是從圖中某一頂點出發,按照某種方法對圖中全部頂點訪問且僅訪問一次。然而, 圖的遍歷要比樹的遍歷複雜得多。 由於圖的任一頂點均可能和其他的頂點相鄰接。 因此在訪問了某個頂點以後, 可能沿着某條路徑搜索以後, 又回到該頂點上。
根據搜索路徑的方向, 一般有兩條遍歷圖的路徑:深度優先遍歷和廣度優先遍歷。 它們對無向圖和有向圖都適用。
深度優先(DepthFirst Search, DFS)遍歷相似千樹的先序遍歷,是樹的先序遍歷的推廣。
對於一個連通圖,深度優先搜索遍歷的過程以下。
初始條件下全部節點爲白色,選擇一個做爲起始頂點,按照以下步驟遍歷:
a. 選擇起始頂點塗成灰色,表示還未訪問
b. 從該頂點的鄰接頂點中選擇一個,繼續這個過程(即再尋找鄰接結點的鄰接結點),一直深刻下去,直到一個頂點沒有鄰接結點了,塗黑它,表示訪問過了
c. 回溯到這個塗黑頂點的上一層頂點,再找這個上一層頂點的其他鄰接結點,繼續如上操做,若是全部鄰接結點往下都訪問過了,就把本身塗黑,再回溯到更上一層。
d. 上一層繼續作如上操做,直到全部頂點都訪問過。
如下面一個有向圖爲例來展現這個過程:
具體代碼實現:
@Override public List<V> dfs(V root) { List<V> visitedList = Guavas.newArrayList(); Stack<V> visitingStack = new Stack<>(); // 頂點首先壓入堆棧 visitingStack.push(root); // 獲取一個邊的節點 while (!visitingStack.isEmpty()) { V visitingVertex = visitingStack.peek(); GraphNode<V> graphNode = getGraphNode(visitingVertex); boolean hasPush = false; if(null != graphNode) { Set<Edge<V>> edgeSet = graphNode.getEdgeSet(); for(Edge<V> edge : edgeSet) { V to = edge.getTo(); if(!visitedList.contains(to) && !visitingStack.contains(to)) { // 尋找到下一個臨接點 visitingStack.push(to); hasPush = true; break; } } } // 循環以後已經結束,沒有找到下一個臨點,則說明訪問結束。 if(!hasPush) { // 獲取第一個元素 visitedList.add(visitingStack.pop()); } } return visitedList; }
廣度優先(Breadth First Search, BFS)遍歷相似於樹的按層次遍歷的過程。
廣度優先搜索在進一步遍歷圖中頂點以前,先訪問當前頂點的全部鄰接結點。
a.首先選擇一個頂點做爲起始結點,並將其染成灰色,其他結點爲白色。
b. 將起始結點放入隊列中。
c. 從隊列首部選出一個頂點,並找出全部與之鄰接的結點,將找到的鄰接結點放入隊列尾部,將已訪問過結點塗成黑色,沒訪問過的結點是白色。若是頂點的顏色是灰色,表示已經發現而且放入了隊列,若是頂點的顏色是白色,表示尚未發現
d. 按照一樣的方法處理隊列中的下一個結點。
基本就是出隊的頂點變成黑色,在隊列裏的是灰色,還沒入隊的是白色。
如下面一個有向圖爲例來展現這個過程:
來看一下具體代碼實現:
@Override public List<V> bfs(final V root) { List<V> visitedList = Guavas.newArrayList(); Queue<V> visitingQueue = new LinkedList<>(); // 1. 放入根節點 visitingQueue.offer(root); // 2. 開始處理 V vertex = visitingQueue.poll(); while (vertex != null) { // 2.1 獲取對應的圖節點 GraphNode<V> graphNode = getGraphNode(vertex); // 2.2 圖節點存在 if(graphNode != null) { Set<Edge<V>> edgeSet = graphNode.getEdgeSet(); //2.3 將不在訪問列表中 && 再也不處理隊列中的元素加入到隊列。 for(Edge<V> edge : edgeSet) { V target = edge.getTo(); if(!visitedList.contains(target) && !visitingQueue.contains(target)) { visitingQueue.offer(target); } } } //3. 更新節點信息 // 3.1 放入已經訪問的列表 visitedList.add(vertex); // 3.2 當節點設置爲最新的元素 vertex = visitingQueue.poll(); } return visitedList; }
上一篇:重學數據結構(6、樹和二叉樹)
本博客爲學習筆記,參考資料以下!
水平有限,不免錯漏,歡迎指正!
參考:
【1】:鄧俊輝 編著. 《數據結構與算法》
【2】:王世民 等編著 . 《數據結構與算法分析》
【3】: Michael T. Goodrich 等編著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:嚴蔚敏、吳偉民 編著 . 《數據結構》
【5】:程傑 編著 . 《大話數據結構》
【6】:圖的理解:存儲結構與鄰接矩陣的Java實現
【7】:java 實現有向圖(Direct Graph)
【8】:數據結構——圖簡介(java代碼實現鄰接矩陣)
【9】:圖的理解:深度優先和廣度優先遍歷及其 Java 實現