教你在 Java 中实现 Dijkstra 最短路算法的方法

目录
  • 定义
  • 带权有向图的实现
    • 带权有向边
    • 带权有向图
  • 最短路算法
    • API
    • Dijkstra 算法
  • 算法流程
    • 最小索引优先队列
    • 实现算法
  • 后记

定义

最短路问题的定义为:

下图左侧是一幅带权有向图,以顶点 0 为起点到各个顶点的最短路径形成的最短路径树如下图右侧所示:

带权有向图的实现

在实现最短路算法之前需要先实现带权有向图。在上一篇博客 《如何在 Java 中实现最小生成树算法》 中我们实现了带权无向图,只需一点修改就能实现带权有向图。

带权有向边

首先应该实现带权有向图中的边 DirectedEdge,这个类有三个成员变量:指出边的顶点 v、边指向的顶点 w 和边的权重 weight。代码如下所示:

package com.zhiyiyo.graph;
/**
 * 带权有向边
 */
public class DirectedEdge {
    int v, w;
    double weight;
    public DirectedEdge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }
    public int from() {
        return v;
    public int to() {
        return w;
    public double getWeight() {
        return weight;
    @Override
    public String toString() {
        return String.format("%d->%d(%.2f)", v, w, weight);
}

带权有向图

带权有向图的实现非常简单,只需将带权无向图使用的 Edge 类换成 DirectedEdge 类,并作出少许调整即可:

package com.zhiyiyo.graph;
import com.zhiyiyo.collection.stack.LinkStack;
import com.zhiyiyo.collection.stack.Stack;
public class WeightedDigraph {
    private final int V;
    protected int E;
    protected LinkStack<DirectedEdge>[] adj;
    public WeightedDigraph(int V) {
        this.V = V;
        adj = (LinkStack<DirectedEdge>[]) new LinkStack[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new LinkStack<>();
        }
    }
    public int V() {
        return V;
    }
    public int E() {
        return E;
    }
    public void addEdge(DirectedEdge edge) {
        adj[edge.from()].push(edge);
        E++;
    }
    public Iterable<DirectedEdge> adj(int v) {
        return adj[v];
    }
    public Iterable<DirectedEdge> edges() {
        Stack<DirectedEdge> edges = new LinkStack<>();
        for (int v = 0; v < V; ++v) {
            for (DirectedEdge edge : adj(v)) {
                edges.push(edge);
            }
        }
        return edges;
    }
}

最短路算法

API

最短路算法应该支持起始点 \(v_s\) 到任意顶点 \(v_t\) 的最短距离和最短路径的查询:

package com.zhiyiyo.graph;
import com.zhiyiyo.collection.stack.LinkStack;
import com.zhiyiyo.collection.stack.Stack;
public class WeightedDigraph {
    private final int V;
    protected int E;
    protected LinkStack<DirectedEdge>[] adj;
    public WeightedDigraph(int V) {
        this.V = V;
        adj = (LinkStack<DirectedEdge>[]) new LinkStack[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new LinkStack<>();
        }
    }
    public int V() {
        return V;
    public int E() {
        return E;
    public void addEdge(DirectedEdge edge) {
        adj[edge.from()].push(edge);
        E++;
    public Iterable<DirectedEdge> adj(int v) {
        return adj[v];
    public Iterable<DirectedEdge> edges() {
        Stack<DirectedEdge> edges = new LinkStack<>();
        for (int v = 0; v < V; ++v) {
            for (DirectedEdge edge : adj(v)) {
                edges.push(edge);
            }
        return edges;
}

Dijkstra 算法

我们可以使用一个距离数组 distTo[] 来保存起始点 \(v_s\) 到其余顶点 \(v_t\) 的最短路径,且 distTo[] 数组满足以下条件:

可以使用 Double.POSITIVE_INFINITY 来表示无穷大,有了这个数组之后我们可以实现 ShortestPath 前两个方法:

package com.zhiyiyo.graph;
public class DijkstraSP implements ShortestPath {
    private double[] distTo;
    @Override
    public double distTo(int v) {
        return distTo[v];
    }
    public boolean hasPathTo(int v) {
        return distTo[v] < Double.POSITIVE_INFINITY;
}

为了实现保存 \(v_s\) 到 \(v_t\) 的最短路径,可以使用一个边数组 edgeTo[],其中 edgeTo[v] = e_wv 表示要想到达 \(v_t\),需要先经过顶点 \(v_w\),接着从 edgeTo[w]获取到达 \(v_w\) 之前需要到达的上一个节点,重复上述步骤直到发现 edgeTo[i] = null,这时候就说明我们回到了 \(v_s\)。 获取最短路径的代码如下所示:

@Override
public Iterable<DirectedEdge> pathTo(int v) {
    if (!hasPathTo(v)) return null;
    Stack<DirectedEdge> path = new LinkStack<>();
    for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
        path.push(e);
    }
    return path;
}

算法流程

虽然我们已经实现了上述接口,但是如何得到 distTo[]edgeTo[] 还是个问题,这就需要用到 Dijkstra 算法了。算法的思想是这样的:

  • 初始化 distTo[] 使得除了 distTo[s] = 0 外,其余的元素都为 Double.POSITIVE_INFINITY。同时初始化 edgeTo[] 的每个元素都是 null
  • 将顶点 s 的所有相邻顶点 \(v_j\) 加入集合 \(V'\) 中,设置 distTo[j] = l_sj 即初始化最短距离为邻边的权重;
  • 从 \(V'\) 中取出距离最短即 distTo[m] 最小的顶点 \(v_m\),遍历 \(v_m\) 的所有邻边 \((v_m, v_w)\),如果有 \(l_{mw}+l_{sw}<l_{sw}\),就说明从 \(v_s\) 走到 \(v_m\) 再一步走到 \(v_w\) 距离最短,我们就去更新 distTo[m],同时将 \(v_w\) 添加到 \(V'\) 中(如果 \(v_w\) 不在的话);

重复上述过程直到 \(V'\) 变为空,我们就已经找到了所有 \(v_s\) 可达的顶点的最短路径。

上述过程中有个地方会影响算法的性能,就是如何从 \(V'\) 中取出最小距离对应的顶点 \(v_m\)。如果直接遍历 \(V'\) 最坏情况下时间复杂度为 \(O(|V|)\),如果换成最小索引优先队列则可以将时间复杂度降至 \(O(\log|V|)\)。

最小索引优先队列

上一篇博客《如何在 Java 中实现最小生成树算法》 中介绍了最小堆的使用,最小堆可以在对数时间内取出数据集合中的最小值,对应到最短路算法中就是最短路径。但是有一个问题,就是我们想要的是最短路径对应的那个顶点 \(v_m\),只使用最小堆是做不到这一点的。如何能将最小堆中的距离值和顶点进行绑定呢?这就要用到索引优先队列。

索引优先队列的 API 如下所示,可以看到每个元素 item 都和一个索引 k 进行绑定,我们可以通过索引 k 读写优先队列中的元素。想象一下堆中的所有元素放在一个数组 pq 中,索引优先队列可以做到在对数时间内取出 pq 的最小值。

package com.zhiyiyo.collection.queue;
/**
 * 索引优先队列
 */
public interface IndexPriorQueue<K extends Comparable<K>> {
    /**
     * 向堆中插入一个元素
     *
     * @param k 元素的索引
     * @param item 插入的元素
     */
    void insert(int k, K item);
     * 修改堆中指定索引的元素值
     * @param item 新的元素值
    void change(int k, K item);
     * 向堆中插入或修改元素
    void set(int k, K item);
     * 堆是否包含索引为 k 的元素
     * @param k 索引
     * @return 是否包含
    boolean contains(int k);
     * 弹出堆顶的元素并返回其索引
     * @return 堆顶元素的索引
    int pop();
     * 弹出堆中索引为 k 为元素
     * @return 索引对应的元素
    K delete(int k);
     * 获取堆中索引为 k 的元素,如果 k 不存在则返回 null
     * @return 索引为 k 的元素
    K get(int k);
     * 获取堆中的元素个数
    int size();
     * 堆是否为空
    boolean isEmpty();
}

实现索引优先队列比优先队列麻烦一点,因为需要维护每个元素的索引。之前我们是将元素按照完全二叉树的存放顺序进行存储,现在可以换成索引,而元素只需根据索引值 k 放在数组 keys[k] 处即可。只有索引数组 indexes[] 和元素数组 keys[] 还不够,如果我们想实现 contains(int k) 方法,目前只能遍历一下 indexes[],看看 k 在不在里面,时间复杂度是 \(O(|V|)\)。何不多维护一个数组 nodeIndexes[],使得它满足下述关系:

如果能在 nodeIndexes[k] 不是 -1,就说明索引 \(k\) 对应的元素存在与堆中,且索引 k 在 indexes[] 中的位置为 \(d\),即有下述等式成立:

有了这三个数组之后我们就可以实现最小索引优先队列了:

package com.zhiyiyo.collection.queue;
import java.util.Arrays;
import java.util.NoSuchElementException;
/**
 * 最小索引优先队列
 */
public class IndexMinPriorQueue<K extends Comparable<K>> implements IndexPriorQueue<K> {
    private K[] keys;           // 元素
    private int[] indexes;      // 元素的索引,按照最小堆的顺序摆放
    private int[] nodeIndexes;  // 元素的索引在完全二叉树中的编号
    private int N;
    public IndexMinPriorQueue(int maxSize) {
        keys = (K[]) new Comparable[maxSize + 1];
        indexes = new int[maxSize + 1];
        nodeIndexes = new int[maxSize + 1];
        Arrays.fill(nodeIndexes, -1);
    }
    @Override
    public void insert(int k, K item) {
        keys[k] = item;
        indexes[++N] = k;
        nodeIndexes[k] = N;
        swim(N);
    public void change(int k, K item) {
        validateIndex(k);
        swim(nodeIndexes[k]);
        sink(nodeIndexes[k]);
    public void set(int k, K item) {
        if (!contains(k)) {
            insert(k, item);
        } else {
            change(k, item);
        }
    public boolean contains(int k) {
        return nodeIndexes[k] != -1;
    public int pop() {
        int k = indexes[1];
        delete(k);
        return k;
    public K delete(int k) {
        K item = keys[k];
        // 交换之后 nodeIndexes[k] 发生变化,必须先保存为局部变量
        int nodeIndex = nodeIndexes[k];
        swap(nodeIndex, N--);
        // 必须有上浮的操作,交换后的元素可能比上面的元素更小
        swim(nodeIndex);
        sink(nodeIndex);
        keys[k] = null;
        nodeIndexes[k] = -1;
        return item;
    public K get(int k) {
        return contains(k) ? keys[k] : null;
    public K min() {
        return keys[indexes[1]];
    /**
     * 获取最小的元素对应的索引
     */
    public int minIndex() {
        return indexes[1];
    public int size() {
        return N;
    public boolean isEmpty() {
        return N == 0;
     * 元素上浮
     *
     * @param k 元素的索引
    private void swim(int k) {
        while (k > 1 && less(k, k / 2)) {
            swap(k, k / 2);
            k /= 2;
     * 元素下沉
    private void sink(int k) {
        while (2 * k <= N) {
            int j = 2 * k;
            // 检查是否有两个子节点
            if (j < N && less(j + 1, j)) j++;
            if (less(k, j)) break;
            swap(k, j);
            k = j;
     * 交换完全二叉树中编号为 a 和 b 的节点
     * @param a 索引 a
     * @param b 索引 b
    private void swap(int a, int b) {
        int k1 = indexes[a], k2 = indexes[b];
        nodeIndexes[k2] = a;
        nodeIndexes[k1] = b;
        indexes[a] = k2;
        indexes[b] = k1;
    private boolean less(int a, int b) {
        return keys[indexes[a]].compareTo(keys[indexes[b]]) < 0;
    private void validateIndex(int k) {
            throw new NoSuchElementException("索引" + k + "不在优先队列中");
}

注意对比最小堆和最小索引堆的 swap(int a, int b) 方法以及 less(int a, int b) 方法,在交换堆中的元素时使用的依据是元素的大小,交换之后无需调整 keys[],而是交换 nodeIndexes[]indexes[] 中的元素。

实现算法

通过上述的分析,实现 Dijkstra 算法就很简单了,时间复杂度为 \(O(|E|\log |V|)\):

package com.zhiyiyo.graph;
import com.zhiyiyo.collection.queue.IndexMinPriorQueue;
import com.zhiyiyo.collection.stack.LinkStack;
import com.zhiyiyo.collection.stack.Stack;
import java.util.Arrays;
public class DijkstraSP implements ShortestPath {
    private double[] distTo;
    private DirectedEdge[] edgeTo;
    private IndexMinPriorQueue<Double> pq;
    private int s;
    public DijkstraSP(WeightedDigraph graph, int s) {
        pq = new IndexMinPriorQueue<>(graph.V());
        edgeTo = new DirectedEdge[graph.V()];

        // 初始化距离
        distTo = new double[graph.V()];
        Arrays.fill(distTo, Double.POSITIVE_INFINITY);
        distTo[s] = 0;
        visit(graph, s);
        while (!pq.isEmpty()) {
            visit(graph, pq.pop());
        }
    }
    private void visit(WeightedDigraph graph, int v) {
        for (DirectedEdge edge : graph.adj(v)) {
            int w = edge.to();
            if (distTo[w] > distTo[v] + edge.getWeight()) {
                distTo[w] = distTo[v] + edge.getWeight();
                edgeTo[w] = edge;
                pq.set(w, distTo[w]);
            }
    // 省略已实现的方法 ...
}

后记

Dijkstra 算法还能继续优化,将最小索引堆换成斐波那契堆之后时间复杂度为 \(O(|E|+|V|\log |V|)\),这里就不写了(因为还没学到斐波那契堆),以上~~

到此这篇关于教你在 Java 中实现 Dijkstra 最短路算法的方法的文章就介绍到这了,更多相关Java实现 Dijkstra 最短路算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java实现Dijkstra最短路径算法

    任务描述:在一个无向图中,获取起始节点到所有其他节点的最短路径描述 Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止. Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表方式 用OPEN,CLOSE表的方式,其采用的是贪心法的算法策略,大概过程如下: 1.声明两个集合,open和close,open用于存储未遍历的节点,close用来存储

  • java实现最短路径算法之Dijkstra算法

    前言 Dijkstra算法是最短路径算法中为人熟知的一种,是单起点全路径算法.该算法被称为是"贪心算法"的成功典范.本文接下来将尝试以最通俗的语言来介绍这个伟大的算法,并赋予java实现代码. 一.知识准备: 1.表示图的数据结构 用于存储图的数据结构有多种,本算法中笔者使用的是邻接矩阵. 图的邻接矩阵存储方式是用两个数组来表示图.一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息. 设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为: 从上面可以看出,无

  • java实现dijkstra最短路径寻路算法

    [引用]迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径. 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止. 基本思想 通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算). 此外,引进两个集合S和U.S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离). 初始时,S中只有起点s:U中是除s之外的顶点,并且U中顶点的路径是

  • java使用Dijkstra算法实现单源最短路径

    单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径.在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子结构性质. 一.最短路径的最优子结构性质 该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的中间顶点,那么P(k,s)必定是从k到s的最短路径.下面证明该性质的正确性. 假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,

  • 教你在 Java 中实现 Dijkstra 最短路算法的方法

    目录 定义 带权有向图的实现 带权有向边 带权有向图 最短路算法 API Dijkstra 算法 算法流程 最小索引优先队列 实现算法 后记 定义 最短路问题的定义为: 下图左侧是一幅带权有向图,以顶点 0 为起点到各个顶点的最短路径形成的最短路径树如下图右侧所示: 带权有向图的实现 在实现最短路算法之前需要先实现带权有向图.在上一篇博客 <如何在 Java 中实现最小生成树算法> 中我们实现了带权无向图,只需一点修改就能实现带权有向图. 带权有向边 首先应该实现带权有向图中的边 Direct

  • 详解Java中使用泛型实现快速排序算法的方法

    快速排序算法概念 快速排序一般基于递归实现.其思路是这样的: 1.选定一个合适的值(理想情况中值最好,但实现中一般使用数组第一个值),称为"枢轴"(pivot). 2.基于这个值,将数组分为两部分,较小的分在左边,较大的分在右边. 3.可以肯定,如此一轮下来,这个枢轴的位置一定在最终位置上. 4.对两个子数组分别重复上述过程,直到每个数组只有一个元素. 5.排序完成. 基本实现方式: public static void quickSort(int[] arr){ qsort(arr,

  • java中this与super关键字的使用方法

    java中this与super关键字的使用方法 这几天看到类在继承时会用到this和super,这里就做了一点总结,与各位共同交流,有错误请各位指正~ this this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针. this的用法在java中大体可以分为3种: 1.普通的直接引用 这种就不用讲了,this相当于是指向当前对象本身. 2.形参与成员名字重名,用this来区分: class Person { private int age = 10; public Perso

  • Java中获取类路径classpath的简单方法(推荐)

    如下所示: <SPAN style="FONT-SIZE: 18px"> System.out.println("++++++++++++++++++++++++"); String path = System.getProperty("java.class.path"); String path2 = FreeMarkerWriter.class.getProtectionDomain().getCodeSource().getLo

  • Java中数组的创建与传参方法(学习小结)

    (一)数组的创建 数组的创建包括两部分:数组的申明与分配内存空间. int score[]=null; //申明一维数组 score=new int[3]; //分配长度为3的空间 数组的申明还有另外一种方式: int[] score=null; //把中括号写在数组名前面 通常,在写代码时,为了方便,我们将两行合并为一行: int score[]=new int score[3]; //将数组申明与分配内存写在一行 (二)传递参数 由于初学java,这里只讨论值传递,不考虑地址传递.主要有3点

  • java中建立0-10m的消息(字符串)实现方法

    直接用StringBuilder,它的append方法方便快速构建字符串. StringBuilder sb1=new StringBuilder(); for(int i=0;i<1024*1024*10;i++){ sb1.append('a'+""); } 取消息时 String str=sb1.tostring(); 取1M str.substring(0, 1024*1024)).getBytes(); 以上这篇java中建立0-10m的消息(字符串)实现方法就是小编分

  • Java中Json字符串直接转换为对象的方法(包括多层List集合)

    使用到的类:net.sf.json.JSONObject 使用JSON时,除了要导入JSON网站上面下载的json-lib-2.2-jdk15.jar包之外,还必须有其它几个依赖包:commons-beanutils.jar,commons-httpclient.jar,commons-lang.jar,ezmorph.jar,morph-1.0.1.jar 下面是例子代码: // JSON转换 JSONObject jsonObj = JSONObject.fromObject(jsonStr

  • java 中ArrayList迭代的两种实现方法

    java 中ArrayList迭代的两种实现方法 Iterator与for语句的结合来实现,代码很简单,大家参考下. 实现代码: package cn.us; import java.util.ArrayList; import java.util.Iterator; //ArrayList迭代的两种方法 //Iterator与for语句的结合 public class Test1 { public static void main(String[] args) { ArrayList arra

  • java中接口(interface)及使用方法示例

    1.接口:一种把类抽象的更彻底,接口里只能包含抽象方法的"特殊类".接口不关心类的内部状态数据,定义的是一批类所遵守的规范.(它只规定这批类里必须提供某些方法,提供这些方法就可以满足实际要求). 在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明.一个类通过继承接口的方式,从而来继承接口的抽象方法. 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念.类描述对象的属性和方法.接口则包含类要实现的方法. 除非实现接口的类是抽象类,否则该类

  • java中申请不定长度数组ArrayList的方法

    如下所示: import java.util.ArrayList; //java中申请不定长度数组 public class Test01 { public static void main(String[] args) { // TODO Auto-generated method stub ArrayList list=new ArrayList(); list.add("123"); list.add("5"); list.add("5")

随机推荐