資料結構:圖(Graph)
圖看起來就像下圖這樣:
在電腦科學中,一個圖就是一些頂點的集合,這些頂點透過一系列邊結對(連線)。頂點用圓圈表示,邊就是這些圓圈之間的連線。頂點之間透過邊連線。
注意:頂點有時也稱為節點或者交點,邊有時也稱為連結。
一個圖可以表示一個社交網路,每一個人就是一個頂點,互相認識的人之間透過邊聯絡。
圖有各種形狀和大小。邊可以有權重(weight),即每一條邊會被分配一個正數或者負數值。考慮一個代表航線的圖。各個城市就是頂點,航線就是邊。那麼邊的權重可以是飛行時間,或者機票價格。
有了這樣一張假設的航線圖。從舊金山到莫斯科最便宜的路線是到紐約轉機。
邊可以是有方向的。在上面提到的例子中,邊是沒有方向的。例如,如果 Ada 認識 Charles,那麼 Charles 也就認識 Ada。相反,有方向的邊意味著是單方面的關係。一條從頂點 X 到 頂點 Y 的邊是將 X 聯向 Y,不是將 Y 聯向 X。
繼續前面航班的例子,從舊金山到阿拉斯加的朱諾有向邊意味著從舊金山到朱諾有航班,但是從朱諾到舊金山沒有(我假設那樣意味著你需要走回去)。
下面的兩種情況也是屬於圖:
左邊的是樹,右邊的是連結串列。他們都可以被當成是樹,只不過是一種更簡單的形式。他們都有頂點(節點)和邊(連線)。
第一種圖包含圈(cycles),即你可以從一個頂點出發,沿著一條路勁最終會回到最初的頂點。樹是不包含圈的圖。
另一種常見的圖型別是單向圖或者 DAG:
就像樹一樣,這個圖沒有任何圈(無論你從哪一個節點出發,你都無法回到最初的節點),但是這個圖有有向邊(透過一個箭頭表示,這裡的箭頭不表示繼承關係)。
為什麼要使用圖?
也許你聳聳肩然後心裡想著,有什麼大不了的。好吧,事實證明圖是一種有用的資料結構。
如果你有一個程式設計問題可以透過頂點和邊表示出來,那麼你就可以將你的問題用圖畫出來,然後使用著名的圖演算法(比如廣度優先搜尋 或者 深度優先搜尋)來找到解決方案。
例如,假設你有一系列任務需要完成,但是有的任務必須等待其他任務完成後才可以開始。你可以透過非迴圈有向圖來建立模型:
每一個頂點代表一個任務。兩個任務之間的邊表示目的任務必須等到源任務完成後才可以開始。比如,在任務B和任務D都完成之前,任務C不可以開始。在任務A完成之前,任務A和D都不能開始。
現在這個問題就透過圖描述清楚了,你可以使用深度優先搜尋演算法來執行執行拓撲排序。這樣就可以將所有的任務排入最優的執行順序,保證等待任務完成的時間最小化。(這裡可能的順序之一是:A, B, D, E, C, F, G, H, I, J, K)
不管是什麼時候遇到困難的程式設計問題,問一問自己:“如何用圖來表述這個問題?”。圖都是用於表示資料之間的關係。 訣竅在於如何定義“關係”。
如果你是一個音樂家你可能會喜歡這個圖:
這些頂點來自C大調的和絃。這些邊--表示和絃之間的關係--描述了怎樣從一個和絃到另一個和絃。這是一個有向圖,所以箭頭的方向表示了怎樣從一個和絃到下一個和絃。它同時還是一個加權圖,每一條邊的權重(這裡用線條的寬度來表示)說明了兩個和絃之間的強弱關係。如你所見,G7-和絃後是一個C和絃和一個很輕的 Am 和絃。
程式設計師常用的另一個圖就是狀態機,這裡的邊描述了狀態之間切換的條件。下面這個狀態機描述了一個貓的狀態:
圖真的很棒。Facebook 就從他們的社交圖中賺取了鉅額財富。如果計劃學習任何資料結構,則應該選擇圖,以及大量的標準圖演算法。
頂點和邊
理論上,圖就是一堆頂點和邊物件而已,但是怎麼在程式碼中來描述呢?
有兩種主要的方法:鄰接列表和鄰接矩陣。
鄰接列表:在鄰接列表實現中,每一個頂點會儲存一個從它這裡開始的邊的列表。比如,如果頂點A 有一條邊到B、C和D,那麼A的列表中會有3條邊
鄰接列表只描述了指向外部的邊。A 有一條邊到B,但是B沒有邊到A,所以 A沒有出現在B的鄰接列表中。查詢兩個頂點之間的邊或者權重會比較費時,因為遍歷鄰接列表直到找到為止。
鄰接矩陣:在鄰接矩陣實現中,由行和列都表示頂點,由兩個頂點所決定的矩陣對應元素表示這裡兩個頂點是否相連、如果相連這個值表示的是相連邊的權重。例如,如果從頂點A到頂點B有一條權重為 5.6 的邊,那麼矩陣中第A行第B列的位置的元素值應該是5.6:
往這個圖中新增頂點的成本非常昂貴,因為新的矩陣結果必須重新按照新的行/列建立,然後將已有的資料複製到新的矩陣中。
所以使用哪一個呢?大多數時候,選擇鄰接列表是正確的。下面是兩種實現方法更詳細的比較。
假設 V 表示圖中頂點的個數,E 表示邊的個數。
操作 | 鄰接列表 | 鄰接矩陣 |
---|---|---|
儲存空間 | O(V + E) | O(V^2) |
新增頂點 | O(1) | O(V^2) |
新增邊 | O(1) | O(1) |
檢查相鄰性 | O(V) | O(1) |
“檢查相鄰性” 是指對於給定的頂點,嘗試確定它是否是另一個頂點的鄰居。在鄰接列表中檢查相鄰性的時間複雜度是O(V),因為最壞的情況是一個頂點與每一個頂點都相連。
在 稀疏圖的情況下,每一個頂點都只會和少數幾個頂點相連,這種情況下相鄰列表是最佳選擇。如果這個圖比較密集,每一個頂點都和大多數其他頂點相連,那麼相鄰矩陣更合適。
程式碼:頂點和邊
先看一下邊的定義:
class Edge<T>(val from: Vertex<T>, val to: Vertex<T>, val weight: Double? = 0.toDouble()) { }
邊包含了3個屬性 “from” 和 “to” 頂點,以及權重值。注意 Edge
物件總是有方向的。如果需要新增一條無向邊,你需要在相反方向新增一個 Edge
物件。weight 屬性是可選的,所以加權圖和未加權圖都可以用它們來描述。
Vertex
的定義:
class Vertex<T>(var data: T? = null, var index: Int = 0) { //val edgeList : List<EdgeList<T>> = emptyList() val edges: ArrayList<Edge<T>> = ArrayList() var visited = false //var distance = 0 fun addEdge(edge: Edge<T>){ edges.add(edge) } override fun toString(): String { return data.toString() } }
由於是泛型定義,所以它可以存放任何型別的資料。
圖
注意:圖的實現方式有很多,這裡給出來的只是一種可能的實現。你可以根據不同問題來裁剪這些程式碼。例如你的邊可能不需要
weight
屬性,你也可能不需要區分有向邊和無向邊。
這裡有一個簡單的圖:
我們可以用鄰接列表或者鄰接矩陣來實現。實現這些概念的類都是繼承自通用的 API AbstractGraph
,所以它們可以相同的方式建立,但是背後各自使用不同的資料結構。
我們來建立一個有向加權圖,來儲存上面的資料:
val graph = Graph<Int>() val v1 = graph.createVertex(1) val v2 = graph.createVertex(2) val v3 = graph.createVertex(3) val v4 = graph.createVertex(4) val v5 = graph.createVertex(5) graph.addDirectedEdge(fromVertex = v1, toVertex = v2, weightValue = 1.0) graph.addDirectedEdge(fromVertex = v2, toVertex = v3, weightValue = 1.0) graph.addDirectedEdge(fromVertex = v3, toVertex = v4, weightValue = 4.5) graph.addDirectedEdge(fromVertex = v4, toVertex = v1, weightValue = 2.8) graph.addDirectedEdge(fromVertex = v2, toVertex = v5, weightValue = 3.2) graph.printAdjacencyList()
前面我們已經說過,如果要新增一條無向邊,需要新增兩條有向邊。對於無向圖,我們可以使用下面的程式碼來替換:
graph.addUnDirectedEdge(fromVertex = v1, toVertex = v2, weightValue = 1.0) graph.addUnDirectedEdge(fromVertex = v2, toVertex = v3, weightValue = 1.0) graph.addUnDirectedEdge(fromVertex = v3, toVertex = v4, weightValue = 4.5) graph.addUnDirectedEdge(fromVertex = v4, toVertex = v1, weightValue = 2.8) graph.addUnDirectedEdge(fromVertex = v2, toVertex = v5, weightValue = 3.2)
如果是未加權圖,weight 這個引數我們可以不用傳遞值。
鄰接列表的實現
為了維護鄰接列表,需要一個類(EdgeList)將邊列表對映到一個頂點。然後圖只需要簡單的維護這樣一個物件(EdgeList)的列表就可以,並根據需要修改這個列表。
class EdgeList<T> (var vertex: Vertex<T> ){ var edges: ArrayList<Edge<T>> = ArrayList() fun addEdge(edge: Edge<T>){ edges.add(edge) } }
Graph 的完整實現:
class Graph<T>(private val vertices: ArrayList<Vertex<T>> = ArrayList(), private val adjacencyList: ArrayList<EdgeList<T>> = ArrayList()) { fun createVertex(value: T): Vertex<T> { val matchingVertices = vertices.filter { it.data == value } if (matchingVertices.isNotEmpty()) { return matchingVertices.last() } val vertex = Vertex(value, adjacencyList.size) vertices.add(vertex) adjacencyList.add(EdgeList(vertex)) return vertex } fun addDirectedEdge(fromVertex: Vertex<T>, toVertex: Vertex<T>, weightValue: Double) { val edge = Edge(from = fromVertex, to = toVertex, weight = weightValue) fromVertex.addEdge(edge) val fromIndex = vertices.indexOf(fromVertex) adjacencyList[fromIndex].edges.add(edge) } fun addUnDirectedEdge(fromVertex: Vertex<T>, toVertex: Vertex<T>, weightValue: Double = 0.0) { addDirectedEdge(fromVertex, toVertex, weightValue) addDirectedEdge(toVertex, fromVertex, weightValue) } fun printAdjacencyList() { (0 until vertices.size) .filterNot { adjacencyList[it].edges.isEmpty() } .forEach { println("""${vertices[it].data} ->[${adjacencyList[it].edges.joinToString()}] """) } } }
來測試一下上面的那個航線圖:
val planeGraph = Graph<String>() val hk = planeGraph.createVertex("Hong Kong") val ny = planeGraph.createVertex("New York") val mosc = planeGraph.createVertex("Moscow") val ld = planeGraph.createVertex("London") val pairs = planeGraph.createVertex("Pairs") val am = planeGraph.createVertex("Amsterdam") val sf = planeGraph.createVertex("San Francisco") val ja = planeGraph.createVertex("Juneau Alaska") val tm = planeGraph.createVertex("Timbuktu") planeGraph.addUnDirectedEdge(hk, sf, 500.0) planeGraph.addUnDirectedEdge(hk,mosc,900.0) planeGraph.addDirectedEdge(sf, ja, 300.0) planeGraph.addUnDirectedEdge(sf, ny, 150.0) planeGraph.addDirectedEdge(mosc,ny, 750.0) planeGraph.addDirectedEdge(ld, mosc, 200.0) planeGraph.addUnDirectedEdge(ld, pairs, 70.0) planeGraph.addDirectedEdge(sf,pairs, 800.0) planeGraph.addUnDirectedEdge(pairs, tm, 250.0) planeGraph.addDirectedEdge(am, pairs, 50.0) planeGraph.printAdjacencyList()
執行結果如下:
Hong Kong ->[(San Francisco: 500.0), (Moscow: 900.0)] Moscow ->[(New York: 750.0)] London ->[(Moscow: 200.0), (Pairs: 70.0)] Pairs ->[(Timbuktu: 250.0)] Amsterdam ->[(Pairs: 50.0)] San Francisco ->[(Juneau Alaska: 300.0), (New York: 150.0), (Pairs: 800.0)]
作者:唐先僧
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3244/viewspace-2821350/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- js資料結構--圖(graph)JS資料結構
- JS Graph (圖-資料結構)JS資料結構
- 圖(Graph)——圖的儲存結構
- 資料結構 - 圖資料結構
- 【資料結構——圖和圖的儲存結構】資料結構
- 資料結構之圖資料結構
- 圖資料庫 Nebula Graph 的資料模型和系統架構設計資料庫模型架構
- 圖資料庫 Nebula Graph TTL 特性資料庫
- [譯文] 初學者應該瞭解的資料結構: Graph資料結構
- 資料結構:圖的表示資料結構
- 資料結構學習總結--圖資料結構
- 資料結構與演算法:圖形結構資料結構演算法
- 圖解Java常用資料結構圖解Java資料結構
- 重學資料結構(七、圖)資料結構
- 【PHP資料結構】圖的概念和儲存結構PHP資料結構
- 圖資料庫 Nebula Graph 的安裝部署資料庫
- JavaScript資料結構——圖的實現JavaScript資料結構
- 資料結構(java)圖論作業資料結構Java圖論
- 資料結構第七節(圖(中))資料結構
- 為知識的海洋繪製地圖 —— 利用CirroData-Graph圖資料庫構建知識圖譜地圖資料庫
- 結構化資料、半結構化資料和非結構化資料
- 淺析圖資料庫 Nebula Graph 資料匯入工具——Spark Writer資料庫Spark
- 資料結構學習(C++)——圖(總結) (轉)資料結構C++
- GraphX 在圖資料庫 Nebula Graph 的圖計算實踐資料庫
- 大話資料結構-思維導圖資料結構
- 資料結構——圖相關基本概念資料結構
- 資料結構 - 圖之程式碼實現資料結構
- 資料結構-二叉樹、堆、圖資料結構二叉樹
- 【資料結構篇】認識資料結構資料結構
- 使用圖資料庫 Nebula Graph 資料匯入快速體驗知識圖譜 OwnThink資料庫
- 分散式圖資料庫 Nebula Graph 的 Index 實踐分散式資料庫Index
- 圖資料庫|Nebula Graph v3.1.0 效能報告資料庫
- 圖資料庫|[Nebula Graph v3.1.0 效能報告資料庫
- 資料結構小白系列之資料結構概述資料結構
- 資料結構?資料結構
- 資料結構資料結構
- 資料結構與演算法-圖解版資料結構演算法圖解
- 圖解:Java 中的資料結構及原理圖解Java資料結構