数据结构:图(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)] 

Swift 英文原版戳这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349