【译】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

相关文章
相关标签/搜索