資料結構:圖(Graph)

wangsys發表於2021-09-09

圖看起來就像下圖這樣:

圖片描述

在電腦科學中,一個圖就是一些頂點的集合,這些頂點透過一系列結對(連線)。頂點用圓圈表示,邊就是這些圓圈之間的連線。頂點之間透過邊連線。

注意:頂點有時也稱為節點或者交點,邊有時也稱為連結。

一個圖可以表示一個社交網路,每一個人就是一個頂點,互相認識的人之間透過邊聯絡。

圖片描述

圖有各種形狀和大小。邊可以有權重(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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章