美文网首页
单源最短路径-Dijkstra算法实现

单源最短路径-Dijkstra算法实现

作者: 云飞扬1 | 来源:发表于2020-12-09 20:52 被阅读0次

    记得以前学数据结构时,死活没有搞懂最短路径算法,书本上各种概念讲一大堆,没过两天全给忘了,更不用说手写一个最短路径算法出来。写这篇文章,旨在让一个算法小白能轻松的手写该算法出来。

    1. 什么是单源最短路径?

    首先,先简单介绍一下什么是单源最短路径。一个图(Graph)由很多顶点(Vertex)构成,顶点与顶点之间相连构成边(Edge),每条边都有权重(Weight),从某个起点开始到某个终点,经过的所有路径的权重和加起来最小,这个路径叫做单源最短路径。

    可以想象成一副地图,从你家里到公司上班,地图导航会帮你规划出一个耗时最少的最短路径。家、公司相当于图中的两个顶点,经过每条道路时的耗时相当于边的权重。

    举个例子:


    这是一个有向图,共有 6 个顶点,连接2个顶点的是边,边上的数字就是权重。那么问题来了,怎么求出从顶点 v0 到其他各个顶点的最短路径?比方说从 v0->v1的路径有好几条:v0->v1: 100v0-v2-v1: 90v0-v4-v3-v1: 70v0-v2-v3-v1: 60,可以发现v0-v2-v3-v1这条路径是最短的,其权重加起来只有 60,因此v0-v2-v3-v1这就是从顶点 v0 到顶点 v1 的最短路径。

    2. 图的表示

    图的存储有 2 种方式:一种是邻接矩阵,用一个二维数组来表示,例如 a[i][j] 表示顶点 i 与顶点 j 之间的边的权重值,但是如果顶点之间的边比较少,就会形成稀疏矩阵,浪费存储空间;另一种是邻接表,用一个数组来表示顶点,每个数组里存储的是从该顶点可达的顶点的数据集合信息,我们采用这种方式来表示图:

    //图的顶点编号都从 0 开始
    class MapGraph(val size: Int) {
    
        /**
         * 图中的边,这是一个带权图
         *
         * @param sv 起始顶点
         * @param ev 终止顶点
         * @param weight sv->ev的权重
         */
        class Edge(var sv: Int, var ev: Int, var weight: Int)
    
        //我们采用邻接表来存储顶点和边,节省内存空间
        //顶点编号从 0 开始,adjArray[i] 表示与编号为 i 的顶点相连的顶点以及边的信息
        private val adjArray = Array<MutableMap<Int, Edge>>(size) {
            mutableMapOf()
        }
    
        /**
         * 添加边
         * @param sv 起始顶点
         * @param ev 终止顶点
         * @param weight 权重
         */
        fun addEdge(sv: Int, ev: Int, weight: Int) {
            var edge = Edge(sv, ev, weight)
            adjArray[sv][ev] = edge
        }
    
    }
    

    3. Dijkstra 算法

    大名鼎鼎的Dijkstra算法是根据发明者的名字来命名的,其实看原算法分析,初学者真的很难理解,即便搞懂之后,保证过不了几天,基本上又忘记是咋回事了。

    学习算法,不能死记硬背代码的写法,以前比较容易进入这种误区,结果就是几天之后全给忘了。Dijkstra算法的核心思想有以下几点,都是用我自己的理解来总结的,虽然网上都有完整的定义,但我们总是力求简单明了好理解,适合自己就行:

    1. 首先定义 2 个集合:S、U,S 是已求出最短路径的顶点集合(包含顶点信息以及最短路径值),U 是未求出最短路径的顶点集合;
    2. 刚开始时,S 中只有一个顶点就是起始顶点,因为它到自己本身的最短路径为 0,并且这肯定是最短路径。将其当做目标顶点,将目标顶点与其直接相邻的顶点放在 U 中,不直接可达的我们认为其路径值为无穷大;
    3. 找出 U 中路径值最小的顶点,从 U 中移除并放入 S 中,同样将其叫做目标顶点。遍历目标顶点所有相邻的顶点,计算直接从该目标顶点到邻接顶点的路径值,如果邻接顶点在集合 U 中不存在则直接放入 U 中,如果 U 中已存在则比较新的路径值与原路径值,最终将邻接顶点的路径值更新为较小的值;
    4. 重复步骤 3 直到找到目标顶点或者没有找到退出循环;

    以前面那个图为例,我们来求从顶点 v0 到顶点 v1 的最短路径。


    代码如下(kotlin实现):

        /**
         * @param v 顶点编号
         * @param dist 起始顶点到该顶点的距离
         */
        inner class VertexDist(var v: Int, var dist: Int)
    
        /**
         * @param sv 起始顶点编号
         * @param end 目标顶点编号
         * @return
         */
        fun dijkstra(sv: Int, end: Int): Int {
            if (sv == end) {
                return 0
            }
            //集合s,这里的每个顶点,都已经计算出从起始顶点到该顶点的最短路径
            var s = mutableListOf<VertexDist>()
            //集合u,暂时记录当前能计算出的最短路径,但不一定是最短路径
            var u = mutableMapOf<Int, VertexDist>()
            //记录前驱节点
            var predecessor = mutableMapOf<Int, Int>()
    
            //BaseCase: 起始顶点自己到自己的距离肯定为 0
            s.add(VertexDist(sv, 0))
            //BaseCase:与其相邻直接可达的顶点放入集合 U 中
            for (edge in adjArray[sv].values) {
                u[edge.ev] = VertexDist(edge.ev, edge.weight)
                predecessor[edge.ev] = edge.sv
            }
            while (u.isNotEmpty()) {
                //从集合 u 中找出路径值最小的顶点 minVertex 放入集合 s 中
                var minVertex = findMinVertex(u)
                u.remove(minVertex.v)
                s.add(minVertex)
                //如果已经找到目标顶点
                if (minVertex.v == end) {
                    //打印最短路径
                    var list = mutableListOf<Int>()
                    var curr = minVertex.v
                    list.add(curr)
                    while (predecessor[curr] != null) {
                        curr = predecessor[curr]!!
                        list.add(curr)
                    }
                    list.reverse()
                    for (i in list.indices) {
                        print("${list[i]}${if (i < list.size - 1) "->" else ""}")
                    }
                    println()
                    return minVertex.dist
                }
    
                //更新能通过顶点 minVertex 到达的所有顶点最小路径值
                var nextVertexes = adjArray[minVertex.v]
                for (edge in nextVertexes.values) {
                    var tmpDist = minVertex.dist + edge.weight
                    //顶点不在集合 u 中,则加入
                    if (u[edge.ev] == null) {
                        u[edge.ev] = VertexDist(edge.ev, tmpDist)
                        predecessor[edge.ev] = edge.sv
                    } else {
                        //顶点已经在集合 u 中了,如果经过 minVertex 到达该顶点的路径值更小,则更新
                        if (tmpDist < u[edge.ev]!!.dist) {
                            u[edge.ev]!!.dist = tmpDist
                            predecessor[edge.ev] = edge.sv
                        }
                    }
                }
            }
            println("没有可达的路径")
            return Int.MAX_VALUE
        }
    
        //找出路径值最小的顶点
        private fun findMinVertex(map: Map<Int, VertexDist>): VertexDist {
            var tmp: VertexDist? = null
            for (vertex in map.values) {
                if (tmp == null || vertex.dist < tmp.dist)
                    tmp = vertex
            }
            return tmp!!
        }
    

    上面代码中有个问题,findMinVertex 方法需要遍历查找所有的顶点,其时间复杂度为 O(n),效率会比较低,那么有没什么好的方式呢?我们以前学过堆这个概念,对堆元素的增加、删除的时间复杂度均为O(logn),小顶堆的堆顶元素就是集合里的最小值,如果我们将集合 U 设计成一个类似堆的数据结构,则可以大大提升效率,这里我就不实现了。

    4. 小结

    怎么记住这个算法呢,记住 2 个核心关键点:一是有2个集合 S、U,知道它分别代表的什么意思,二是怎么更新集合 U 中顶点的最短距离值。剩下手写代码的事情,无非是在这个框架之上修修补补的事情了。

    相关文章

      网友评论

          本文标题:单源最短路径-Dijkstra算法实现

          本文链接:https://www.haomeiwen.com/subject/narkgktx.html