zoukankan      html  css  js  c++  java
  • 数据结构:图(Graph)

    数据结构:图(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 英文原版戳这里

    小礼物走一走,来简书关注我

    赞赏支持

  • 相关阅读:
    环境是如何建立的 启动文件有什么
    环境中存储的是什么
    串行 并行 异步 同步
    TPC-H is a Decision Support Benchmark
    进程通信类型 管道是Linux支持的最初Unix IPC形式之一 命名管道 匿名管道
    删除环境变量
    14.3.2.2 autocommit, Commit, and Rollback 自动提交 提交和回滚
    14.3.2.2 autocommit, Commit, and Rollback 自动提交 提交和回滚
    14.3.2.1 Transaction Isolation Levels 事务隔离级别
    14.3.2.1 Transaction Isolation Levels 事务隔离级别
  • 原文地址:https://www.cnblogs.com/grj001/p/12224662.html
Copyright © 2011-2022 走看看