数据结构与算法之最短路径

公交和地铁是最普遍的交通工具了,但是通常情况下去往某处有多种出行方案,有的少换乘,有的时间短,有的步行少,等等。这就涉及到如何寻找一条最合适的路径的问题,比如从下图的v0处出发,怎样才能最快到达v8处?

最短路径

寻找最短路径,通常也有两种经典算法,接下来我们一一介绍。

迪杰斯特拉(Dijkstra)算法

迪杰斯特拉算法的思路是从起点开始,寻找它到每个中间点的最短距离,一步步向终点逼近。我们就以上图为例,演示迪杰斯特拉算法的过程。

首先从v0出发,可以到达的最近的点是v1,距离是1,所以通过的第一条边是(v0, v1),如下所示:

v0-v1

接下来因为v1可以去往v2、v3和v4,所以从v0->v1->v2距离为4,从v0->v1->v3距离为8,从v0->v1->v4距离为6,这三条路径中最短为v0->v1->v2,它甚至比直接从v0->v2还要短,所以接下来的路径应该为v0->v1->v2,如下所示:

v0-v2

接下来我们又可以获取更多的路径信息,从v0->v2->v4距离是5,从v0->v2->v5距离是11,而前者比从从v0->v1->v4还要短,所以接下来的路径应该为v0->v2->v4,其中v0->v2实际上是v0->v1->v2,如下所示:

v0-v4

接下来,我们按照同样的逻辑,从v4出发最近的顶点为v3,路径长度为7,而从v0->v1->v3的直接距离为8,所以完整路径为v0->v1->v2->v4->v3,如下所示:

v0-v3

按照同样方式,我们可以得到以下路径,就是我们要找的最短路径,如下所示:

完整路径

可以看到,迪杰斯特拉算法就是通过不断寻找到中间顶点的最短距离和对比顶点间的直接距离,一点点逼近终点的,这比较适合于提前规划好路线,而不能走一步看一步。

弗洛伊德(Floyd)算法

弗洛伊德算法比较精妙,但是也相对较难理解,我们依然以上图为例,演示它的运算过程。

首先,构建它的邻接矩阵表,称为D-1,除此之外,再构建一个矩阵P-1,赋值如下:

初始矩阵

其中上标-1表示初始化状态,P-1中的每个值表示从顶点 vi 到顶点 vj 需要经过的中间点,默认值表示直接连接,没有中间点。

初始化完成后,就可以进行弗洛伊德算法了。首先,让所有顶点之间的路径都经过v0,比如从v1->v2,变为v1->v0->v2,此时发现v1->v2距离为3,而变为v1->v0->v2后距离成为了6,所以不需要更新数据。由于v3-v8都不邻接于v0,也就无法让 v0 成为中间点,所以也不需要更新,因此这次操作后,数据没有任何改变,但是矩阵的状态改变了,从D-1变为D0,P同理,如下所示:

第0次运算

现在,我们以每个顶点之间的路径都经过v1,可以看到v0->v2的距离为5,而v0->v1->v2的距离为4,比直接距离更短,同理v0->v3原本不能直接到达,经过v1后的路径v0->v1->v3距离变为8,v0->v1->v4距离变为6。以v2为起点,到达v3的路径v2->v1->v3距离变为10。以v3为起点,到达v0和v2的距离,以及以v4为起点,到达v0的距离都发生了改变。此时因为将v1作为中间点,使得部分路径缩短,于是更新两个表的值如下:

第1次运算

其中P的某些值更新为1,表示从vi到vj需要经过顶点v1

在此基础上,我们以每个顶点之间的路径都经过v2,再次进行同样的计算,我们找到的第一个需要更新的数据是v0->v4,它的距离是6,而v0->v2->v4的距离为5,所以D[0][4]值更新为5,P[0][4]的值则应该更新为P[0][2]的值,这是因为从v0->v2还有可能经过其它中间点,例如这里就是经过v1,这样我们就把v0->v4的路径从原本的v0->...->v4,变为了从v0->...->v2->v4,只要不断迭代,就可以找到完整的路径。更新结果如下所示:

第2次运算

按照同样的规则,我们对其余数据进行同样的操作,最终可以得到下图的结果:

第8次运算

最后得到的D8表的每个数据表示从顶点 vi 到顶点 vj 的最短距离,而P8表的每个数据表示从顶点 vi 到顶点 vj 需要经过的第一个中间顶点。例如从 v0->v8,首先要经过顶点 v1,完整路径为v0->v1加上v1->v8,而v1->v8需要经过v2,于是完整路径为v0->v1->v2加上v2->v8,迭代下去,可以得到最终的最短路径为v0->v1->v2->v4->v3->v6->v7->v8,如下所示:

完整路径

可以看到,结果和使用迪杰斯特拉算法的结果是一致的。

代码实现

迪杰斯特拉算法

public void dijkstra(AMGraph<String> amGraph, int fromIndex) {
    int len = amGraph.getVertexNum();
    // 存储从fromIndex到其他各顶点的最短路径下标
    int[] p = new int[len];
    // 存储从fromIndex到其他各顶点的最短路径的权值和
    int[] d = new int[len];
    // 标记求得了顶点fromIndex到其他各定点的最短路径
    boolean[] finded = new boolean[len];
    // 初始化数据
    for (int toIndex = 0; toIndex < len; toIndex++) {
        finded[toIndex] = false;
        d[toIndex] = amGraph.getWeight(fromIndex, toIndex);
        p[toIndex] = 0;
    }

    // fromIndex到自己的路径长度为0,并且不需要再求它的最短路径了
    d[fromIndex] = 0;
    finded[fromIndex] = true;

    int min = 0;
    int k = -1;
    // 求fromIndex到toIndex的最短路径
    for (int toIndex = 1; toIndex < len; toIndex++) {
        min = Integer.MAX_VALUE;
        // 寻找距离fromIndex最近的顶点
        for (int i = 0; i < len; i++) {
            if (!finded[i] && d[i] < min) {
                // i 离 fromIndex最近
                k = i;
                min = d[i];
            }
        }

        // 找到了最近的点
        finded[k] = true;

        // 更新剩余顶点的距离值
        for (int i = 0; i < len; i++) {
            // 如果经过 k 之后的距离比直接到 i 的距离近,就更新距离
            if (!finded[i] && amGraph.getWeight(k, i) != Integer.MAX_VALUE && (min + amGraph.getWeight(k, i) < d[i])) {
                d[i] = min + amGraph.getWeight(k, i);
                p[i] = k;
            }
        }

        System.out.println();
        for (int i = 0; i < len; i++) {
            System.out.print(p[i] + "\t");
        }
    }
}

迪杰斯特拉算法的时间复杂度为O(n2),但是它每次只能计算出从某一个顶点开始到其它顶点的最短路径。

弗洛伊德算法

public void floyd(AMGraph<String> amGraph) {
    int len = amGraph.getVertexNum();
    int[][] d = new int[len][len];
    int[][] p = new int[len][len];

    // 初始化d和p数组
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len; j++) {
            d[i][j] = amGraph.getWeight(i, j);
            p[i][j] = j;
        }
    }

    for (int k = 0; k < len; k++) {
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                if (d[i][k] != Integer.MAX_VALUE && d[k][j] != Integer.MAX_VALUE && d[i][j] > d[i][k] + d[k][j]) {
                    d[i][j] = d[i][k] + d[k][j];
                    p[i][j] = p[i][k];
                }
            }
        }
    }

    printD(len, d);
    printP(len, p);
}

弗洛伊德算法十分优雅和简洁,并且可以得到任意顶点间的最短路径,不过因为三层循环的原因,它的时间复杂度为O(n3)。


我是飞机酱,如果您喜欢我的文章,可以关注我~

编程之路,道阻且长。唯,路漫漫其修远兮,吾将上下而求索。

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

推荐阅读更多精彩内容