如何在 Java 中实现 Dijkstra 最短路算法
定义
最短路问题的定义为:设 \(G=(V,E)\) 为连通图,图中各边 \((v_i,v_j)\) 有权 \(l_{ij}\) (\(l_{ij}=\infty\) 表示 \(v_i,v_j\) 间没有边) ,\(v_s,v_t\) 为图中任意两点,求一条道路 \(\mu\),使得它是从 \(v_s\) 到 \(v_t\) 的所有路中总权最小的路,即:\(L(\mu)=\sum_{(v_i,v_j)\in \mu}l_{ij}\) 最小。
下图左侧是一幅带权有向图,以顶点 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;
/**
* 最短路径
*/
public interface ShortestPath {
/**
* 从起点到顶点 v 的最短距离,如果顶点 v 不可达则为无穷大
* @param v 顶点 v
* @return 最短路径
*/
double distTo(int v);
/**
* 是否存在从起点到顶点 v 的路径
* @param v 顶点 v
* @return 是否存在
*/
boolean hasPathTo(int v);
/**
* 从起点到顶点 v 的最短路径,若不存在则返回 null
* @param v 顶点 v
* @return 最短路径
*/
Iterable<DirectedEdge> pathTo(int v);
}
Dijkstra 算法
我们可以使用一个距离数组 distTo[]
来保存起始点 \(v_s\) 到其余顶点 \(v_t\) 的最短路径,且 distTo[]
数组满足以下条件:
可以使用 Double.POSITIVE_INFINITY
来表示无穷大,有了 distTo[]
之后就能实现 ShortestPath
前两个方法:
package com.zhiyiyo.graph;
public class DijkstraSP implements ShortestPath {
private double[] distTo;
@Override
public double distTo(int v) {
return distTo[v];
}
@Override
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_{sm}<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 k 元素的索引
* @param item 新的元素值
*/
void change(int k, K item);
/**
* 向堆中插入或修改元素
* @param k 元素的索引
* @param item 新的元素值
*/
void set(int k, K item);
/**
* 堆是否包含索引为 k 的元素
* @param k 索引
* @return 是否包含
*/
boolean contains(int k);
/**
* 弹出堆顶的元素并返回其索引
* @return 堆顶元素的索引
*/
int pop();
/**
* 弹出堆中索引为 k 为元素
* @param k 索引
* @return 索引对应的元素
*/
K delete(int k);
/**
* 获取堆中索引为 k 的元素,如果 k 不存在则返回 null
* @param k 索引
* @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);
}
@Override
public void change(int k, K item) {
validateIndex(k);
keys[k] = item;
swim(nodeIndexes[k]);
sink(nodeIndexes[k]);
}
@Override
public void set(int k, K item) {
if (!contains(k)) {
insert(k, item);
} else {
change(k, item);
}
}
@Override
public boolean contains(int k) {
return nodeIndexes[k] != -1;
}
@Override
public int pop() {
int k = indexes[1];
delete(k);
return k;
}
@Override
public K delete(int k) {
validateIndex(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;
}
@Override
public K get(int k) {
return contains(k) ? keys[k] : null;
}
public K min() {
return keys[indexes[1]];
}
/**
* 获取最小的元素对应的索引
*/
public int minIndex() {
return indexes[1];
}
@Override
public int size() {
return N;
}
@Override
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;
}
}
/**
* 元素下沉
*
* @param k 元素的索引
*/
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) {
if (!contains(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中实现一个固定最大size的hashMap
如何在JAVA中实现一个固定最大size的hashMap 利用LinkedHashMap的removeEldestEntry方法,重载此方法使得这个map可以增长到最大size,之后每插入一条新的记录 ...
- 如何在java中使用sikuli进行自动化测试
很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,ja ...
- 如何在Java中调用Python代码
有时候,我们会碰到这样的问题:与A同学合作写代码,A同学只会写Python,而不会Java, 而你只会写Java并不擅长Python,并且发现难以用Java来重写对方的代码,这时,就不得不想方设法“调 ...
- 如何在java中跳出当前多重嵌套循环?有几种方法?
如何在java中跳出当前多重嵌套循环?有几种方法? - 两种方法 - 1.在外层循环定义标记 ok: for(int i=0;i<100;i++){ ...
- 用代码说话:如何在Java中实现线程
并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...
- 如何在Java中测试类是否是线程安全的
通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考. 线程安全性测试与典型的单线程测试不同.为了测试一个方法是否是线程安全的,我们需 ...
- 如何在 Java 中实现无向环和有向环的检测
无向环 一个含有环的无向图如下所示,其中有两个环,分别是 0-2-1-0 和 2-3-4-2: 要检测无向图中的环,可以使用深度优先搜索.假设从顶点 0 出发,再走到相邻的顶点 2,接着走到顶点 2 ...
- 如何在 Java 中实现最小生成树算法
定义 在一幅无向图 \(G=(V,E)\) 中,\((u, v)\) 为连接顶点 \(u\) 和顶点 \(v\) 的边,\(w(u,v)\) 为边的权重,若存在边的子集 \(T\subseteq E\ ...
- Dijkstra最短路算法
Dijkstra最短路算法 --转自啊哈磊[坐在马桶上看算法]算法7:Dijkstra最短路算法 上节我们介绍了神奇的只有五行的Floyd最短路算法,它可以方便的求得任意两点的最短路径,这称为“多源最 ...
随机推荐
- Arcgis Server发布的带有透明度的地图服务,调用时不显示透明度问题
问题: 在发布道路地图时候设置地图透明度为50% 使用arcgis API for js 中 ArcGISDynamicMapServiceLayer 调用该地图时,发现透明效果不实现 如下图: 解决 ...
- Linux下使用Google Authenticator配置SSH登录动态验证码
1.一般ssh登录服务器,只需要输入账号和密码.2.本教程的目的:在账号和密码之间再增加一个验证码,只有输入正确的验证码之后,再输入密码才能登录.这样就增强了ssh登录的安全性.3.账号.验证码.密码 ...
- bzoj4032/luoguP4112 [HEOI2015]最短不公共子串(后缀自动机+序列自动机上dp)
bzoj4032/luoguP4112 [HEOI2015]最短不公共子串(后缀自动机+序列自动机上dp) bzoj Luogu 题解时间 给两个小写字母串 $ A $ , $ B $ ,请你计算: ...
- 构造器(constructor)是否可被重写(override)?
构造器不能被继承,因此不能被重写,但可以被重载.
- redis 是什么?都有哪些使用场景?
一.什么是redis 首先要说redis,应该先说一下nosql,NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL",泛指非关系型的数据库.随着互联网 ...
- Atomic 的实现原理
1.直接操作内存,使用Unsafe 这个类 2.使用 getIntVolatile(var1, var2) 获取线程间共享的变量 3.采用CAS的尝试机制(核心所在),代码如下: public fin ...
- Iterator 和 ListIterator 有什么区别?
1.ListIterator 可以在遍历的时候,调用add()添加元素 2.ListIterator提供了更多的一些方法,如previous().hasPrevious() 等
- 学习openldap01
Linux 下openldap的详细介绍,搭建,配置管理,备份,案例 Ldap 服务应用指南 兼容(5.X&6.X) 1.1 Ldap 目录服务介绍 1.1.1 什么是目录服务(activ ...
- maven项目改造成springboot项目
springboot项目其实归根到底就是一个maven项目,通常我们创建springboot项目,只要使用idea中的spring Initializr就可以创建就可以了. 今天我们来讲下如何改造一个 ...
- idea问题之"一个或多个listeners启动失败问题"
org.apache.catalina.core.StandardContext.startInternal 一个或多个listeners启动失败,更多详细信息查看对应的容器日志文件之 "A ...