【譯】Swift算法俱樂部-圖

本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.com網站出品的用Swift實現算法和數據結構的開源項目,目前在GitHub上有18000+⭐️,我初略統計了一下,大概有一百左右個的算法和數據結構,基本上常見的都包含了,是iOSer學習算法和數據結構不錯的資源。
🐙andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學習邊翻譯的項目。因爲能力有限,如發現錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小夥伴一塊兒參與翻譯和學習🤓。固然也歡迎加⭐️,🤩🤩🤩🤨🤪。
本文的翻譯原文和代碼能夠查看🐙swift-algorithm-club-cn/Graphgit


圖(Graph)程序員

這個話題已經有個輔導文章github

圖看上去像下圖:算法

A graph

在計算機科學中,圖形被定義爲一組和與之配對的一組。 點用圓圈表示,邊是它們之間的線。 邊連接點與點。編程

注意: 點有時稱爲「節點」,邊稱爲「連接」。swift

圖能夠表明社交網絡。 每一個人都是一個點,彼此認識的人經過邊連接。 下面是一個有點歷史不許確的例子:數組

Social network

圖具備各類形狀和大小。 當爲每一個邊分配正數或負數,邊能夠具備權重。 考慮一個表明飛機航班的圖示例。 城市用點表示,而航班用邊表示。 而後,邊權重能夠描述航班時間或票價。bash

Airplane flights

有了這個假想的航線,從舊金山(San Francisco)飛往莫斯科(Moscow),通過紐約(New York)這條航線是最便宜的。網絡

邊也能夠有向的。 在上面提到的例子中,邊是無向的。 例如,若是阿達(Ada)能夠到達查爾斯(Charles),那麼查爾斯也能夠到達阿達。 另外一方面,有向邊意味着單向關係。 從點X到點Y的有向邊連接X到Y,但Y不能到X.數據結構

從航班的例子來看,從舊金山到阿拉斯加的朱諾( Juneau, Alaska)的有向的邊代表從舊金山到朱諾的航班,但不是從朱諾到舊金山(我想這意味着你正在走回頭路)的航班。

One-way flights

如下也是圖:

Tree and linked list

左邊是結構,右邊是鏈表。 它們能夠被視爲形式更簡單的圖。 它們都有點(節點)和邊(連接)。

第一個圖(譯註:文章的第一個圖)包括循環,您能夠從點開始,沿着路徑,而後返回到原始點。 樹是沒有這種循環的圖。

另外一種常見類型的圖是有向無環圖(DAG, directed acyclic graph):

DAG

像樹同樣,這個圖沒有任何循環(不管你從哪裏開始,都沒有回到起始點的路徑),可是這個圖的定向邊的形狀不必定造成層次結構。

爲何使用圖?

也許你聳聳肩膀思考,有什麼大不了的? 好吧,事實證實圖是一種有用的數據結構。

若是您遇到編程問題,您能夠將數據表示爲點和邊,那麼您能夠將你的問題繪製爲圖形並使用衆所周知的圖算法,例如廣度優先搜索深度優先搜索找到解決方案。

例如,假設您有一個任務列表,其中某些任務必須先等待其餘任務才能開始。 您可使用非循環有向圖對此進行建模:

Tasks as a graph

每一個點表明一個任務。 兩個點之間的邊意味着必須在目標任務開始以前必須完成源任務。 例如,任務C在B和D完成以前沒法啓動,B和D能夠在A完成以前啓動。

如今使用圖表表示問題,您可使用深度優先搜索來執行拓撲排序。 這將使任務處於最佳順序,以便最大限度地減小等待任務完成所花費的時間。 (這裏可能的一個順序是A,B,D,E,C,F,G,H,I,J,K。)

不管什麼時候遇到困難的編程問題,請問本身,「如何使用圖表示此問題?」 圖是你全部數據之間特定關係。 訣竅在於如何定義「關係」。

若是您是音樂家,您可能會喜歡這張圖:

Chord map

這些點是C大調的和絃。 邊 —— 和絃之間的關係 —— 表明可能一個和絃跟隨另外一個和絃。 這是一個有向圖,所以箭頭的方向顯示瞭如何從一個和絃轉到下一個和絃。 它也是一個權重圖,其中邊的權重 —— 這裏用線條粗細描繪 —— 顯示了兩個和絃之間的強關係。 正如你所看到的,G7和絃極可能後跟一個C和絃,也多是一個Am和絃。

您可能在不知道圖時,已經使用過圖了。 您的數據模型也是圖(來自Apple的Core Data文檔):

Core Data model

程序員使用的另外一個常見圖是狀態機(state machine),其中邊描述了狀態之間轉換的條件。 這是一個模擬個人貓的狀態機:

State machine

圖很棒。 Facebook從他們的社交圖中賺了大錢。 若是要學習任何數據結構,則必須選擇圖和大量標準圖算法。

哦,個人點和邊!

理論上,圖只是一堆點和邊對象,可是如何在代碼中描述它?

有兩種主要策略:鄰接表和鄰接矩陣。

鄰接表(Adjacency List)。在鄰接表實現中,每一個點存儲一個從這個點出發的全部邊的列表。例如,若是點A具備到點B,C和D的邊,則點A將具備包含3個邊的列表。

Adjacency list

鄰接表描述了傳出邊。 A具備到B的邊,可是B沒有返回到A的邊,所以A不出如今B的鄰接表中。在兩個點之間找到邊或權重成本可能很高,由於沒有隨機訪問邊。 您必須遍歷鄰接表,直到找到它爲止。

鄰接矩陣(Adjacency Matrix)。 在鄰接矩陣實現中,具備表示頂點的行和列的矩陣存儲權重以指示頂點是否鏈接以及權重。 例如,若是從點A到點B有一個權重爲5.6的有向邊,那麼點A行和B列交叉的值爲5.6:

Adjacency matrix

向圖添加另外一個點是成本很高,由於必須建立一個新的矩陣結構,並有足夠的空間來容納新的行/列,而且必須將現有結構複製到新的矩陣結構中。

那麼你應該使用哪個? 大多數狀況下,鄰接表是正確的方法。 如下是二者之間更詳細的比較。

V 是圖中點的數量,E 是邊數。 而後咱們有:

操做 鄰接列表 鄰接矩陣
存儲空間 O(V + E) O(V^2)
添加點 O(1) O(V^2)
Add Edge O(1) O(1)
添加邊 O(1) O(1)
檢查鄰接 O(V) O(1)

「檢查鄰接」意味着咱們試圖肯定給定點是另外一個點的直接鄰居。 檢查鄰接表的鄰接的時間是 O(V),由於在最壞的狀況下,點須要鏈接到每一個其餘點。

稀疏圖的狀況下,每一個點僅連接到少數其餘點,鄰接表是存儲邊的最佳方式。 若是圖是密集的,其中每一個點鏈接到大多數其餘點,則優選矩陣。

如下是鄰接表和鄰接矩陣的示例實現:

代碼:邊和點

每一個點的鄰接表由Edge對象組成:

public struct Edge<T>: Equatable where T: Equatable, T: Hashable {

  public let from: Vertex<T>
  public let to: Vertex<T>

  public let weight: Double?

}
複製代碼

此結構描述了「from」和「to」點以及權重值。 請注意,Edge對象始終是有向的,單向鏈接(如上圖中的箭頭所示)。 若是須要無向鏈接,還須要在相反方向添加Edge對象。 每一個Edge可選地存儲權重,所以它們可用於描述權重和無權重圖。

Vertex看起來像這樣:

public struct Vertex<T>: Equatable where T: Equatable, T: Hashable {

  public var data: T
  public let index: Int

}
複製代碼

它存儲了一個能夠表示任意數據泛型T,它是Hashable以強制惟一性,還有Equatable。 點自己也是Equatable

代碼:圖

注意: 有不少方法能夠實現圖。 這裏給出的代碼只是一種可能的實現。 您可能但願根據爲您嘗試解決的每一個問題定製圖代碼。 例如,您的邊可能不須要weight屬性,或者您可能不須要區分有向邊和無向邊。

這是簡單圖的例子:

Demo

咱們能夠將其表示爲鄰接矩陣或鄰接表。 實現這些概念的類都從AbstractGraph繼承了一個通用API,所以它們能夠以相同的方式建立,在幕後具備不一樣的優化數據結構。

讓咱們使用每一個表示建立一些有向權重圖來存儲示例:

for graph in [AdjacencyMatrixGraph<Int>(), AdjacencyListGraph<Int>()] {

  let v1 = graph.createVertex(1)
  let v2 = graph.createVertex(2)
  let v3 = graph.createVertex(3)
  let v4 = graph.createVertex(4)
  let v5 = graph.createVertex(5)

  graph.addDirectedEdge(v1, to: v2, withWeight: 1.0)
  graph.addDirectedEdge(v2, to: v3, withWeight: 1.0)
  graph.addDirectedEdge(v3, to: v4, withWeight: 4.5)
  graph.addDirectedEdge(v4, to: v1, withWeight: 2.8)
  graph.addDirectedEdge(v2, to: v5, withWeight: 3.2)

}
複製代碼

如前所述,要建立無向邊,您須要製做兩個有向邊。 對於無向圖,咱們改成使用如下方法:

graph.addUndirectedEdge(v1, to: v2, withWeight: 1.0)
  graph.addUndirectedEdge(v2, to: v3, withWeight: 1.0)
  graph.addUndirectedEdge(v3, to: v4, withWeight: 4.5)
  graph.addUndirectedEdge(v4, to: v1, withWeight: 2.8)
  graph.addUndirectedEdge(v2, to: v5, withWeight: 3.2)
複製代碼

在任何一種狀況下,咱們均可以提供nil做爲withWeight參數的值來製做無權重圖。

代碼:鄰接表

爲了維護鄰接表,有一個類將邊列表映射到點。 該圖只是維護這些對象的數組,並根據須要修改它們。

private class EdgeList<T> where T: Equatable, T: Hashable {

  var vertex: Vertex<T>
  var edges: [Edge<T>]? = nil

  init(vertex: Vertex<T>) {
    self.vertex = vertex
  }

  func addEdge(_ edge: Edge<T>) {
    edges?.append(edge)
  }

}
複製代碼

它們被實現爲一個類而不是結構,因此咱們能夠經過引用來修改它們,就像將邊添加到新點同樣,源點已經有一個邊列表:

open override func createVertex(_ data: T) -> Vertex<T> {
  // check if the vertex already exists
  let matchingVertices = vertices.filter() { vertex in
    return vertex.data == data
  }

  if matchingVertices.count > 0 {
    return matchingVertices.last!
  }

  // if the vertex doesn't exist, create a new one
  let vertex = Vertex(data: data, index: adjacencyList.count)
  adjacencyList.append(EdgeList(vertex: vertex))
  return vertex
}
複製代碼

該示例的鄰接表以下所示:

v1 -> [(v2: 1.0)]
v2 -> [(v3: 1.0), (v5: 3.2)]
v3 -> [(v4: 4.5)]
v4 -> [(v1: 2.8)]
複製代碼

通常形式a -> [(b: w), ...],表示從ab的邊是存在的,權重爲w(可能有更多a出去的邊)。

代碼:鄰接矩陣

咱們將在二維[[Double?]]數組中追蹤鄰接矩陣。 nil表示沒有邊,而任何其餘值表示給定權重的邊。 若是adjacencyMatrix[i][j]不是nil,則從點i到點j有一條邊。

要使用點索引矩陣,咱們使用Vertex中的index屬性,該屬性是在經過圖對象建立點時分配的。 建立新點時,圖必須調整矩陣的大小:

open override func createVertex(_ data: T) -> Vertex<T> {
  // check if the vertex already exists
  let matchingVertices = vertices.filter() { vertex in
    return vertex.data == data
  }

  if matchingVertices.count > 0 {
    return matchingVertices.last!
  }

  // if the vertex doesn't exist, create a new one
  let vertex = Vertex(data: data, index: adjacencyMatrix.count)

  // Expand each existing row to the right one column.
  for i in 0 ..< adjacencyMatrix.count {
    adjacencyMatrix[i].append(nil)
  }

  // Add one new row at the bottom.
  let newRow = [Double?](repeating: nil, count: adjacencyMatrix.count + 1)
  adjacencyMatrix.append(newRow)

  _vertices.append(vertex)

  return vertex
}
複製代碼

而後鄰接矩陣看起來像這樣:

[[nil, 1.0, nil, nil, nil]    v1
 [nil, nil, 1.0, nil, 3.2]    v2
 [nil, nil, nil, 4.5, nil]    v3
 [2.8, nil, nil, nil, nil]    v4
 [nil, nil, nil, nil, nil]]   v5

  v1   v2   v3   v4   v5
複製代碼

擴展閱讀

本文描述了圖是什麼,以及如何實現基本數據結構。 咱們還有關於圖實際用途的其餘文章,因此也要查看一下!

做者:Donald Pinckney, Matthijs Hollemans
翻譯:Andy Ron
校對:Andy Ron

相關文章
相關標籤/搜索