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

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

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 中顶点的最短距离值。剩下手写代码的事情,无非是在这个框架之上修修补补的事情了。

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

推荐阅读更多精彩内容