最短路径问题是图论研究中的一个经典的算法问题,旨在寻找图中两个节点之间的最短路径,最常用的算法有Dijkstra算法、SPFA算法\Bellman-Ford算法、Floyd算法\Floyd-Warshall算法、Johnson算法等,这篇博客将重点介绍Dijkstra算法。
迪杰斯特拉算法
迪杰斯特拉算法是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题。迪杰斯特拉算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。具体的计算规则我们可以通过下图进行查看。
通过这幅图(如果图片无法正确显示,请通过百度百科查看)我们可以简单的理解迪杰斯特拉算法算法的基础思路,下面我们就通过Java来实现这个算法。
先给出一个无向图
用Dijkstra算法找出以A为起点的单源最短路径步骤如下
算法实现
在具体的实现之前,我们先有一个基础的约定,就是途中的每一个节点我们都用正整数进行编码,相邻两点之间的距离是正整数,图中两个直接相邻两点的距离我们保存到map中,也就是求最短距离我们需要实现这样的一个方法:
public MinStep getMinStep(int start, int end, final HashMap> stepLength);
第一个参数是起始节点的编号,第二个参数是终点节点的编号,第三个参数是图中直接相邻两个节点的距离组成的map。关于第三个参数,会在后面做详细的介绍。这里我们定义一个接口,用于计算两点之间的最短路径,具体如下:
/**
*@Description:
*/
package com.lulei.distance;
import java.util.HashMap;
import com.lulei.distance.bean.MinStep;
public interface Distance {
public static final MinStep UNREACHABLE = new MinStep(false, -1);
/**
* @param start
* @param stepLength
* @Author:lulei
* @Description: 起点到终点的最短路径
*/
public MinStep getMinStep(int start, int end, final HashMap> stepLength);
}
一、方法返回值介绍
上面方法的返回值是我们自定义的一个数据类型,下面我们通过代码来看其具体的数据结构:
/**
*@Description:
*/
package com.lulei.distance.bean;
import java.util.List;
public class MinStep {
private boolean reachable;//是否可达
private int minStep;//最短步长
private List step;//最短路径
public MinStep() {
}
public MinStep(boolean reachable, int minStep) {
this.reachable = reachable;
this.minStep = minStep;
}
public boolean isReachable() {
return reachable;
}
public void setReachable(boolean reachable) {
this.reachable = reachable;
}
public int getMinStep() {
return minStep;
}
public void setMinStep(int minStep) {
this.minStep = minStep;
}
public List getStep() {
return step;
}
public void setStep(List step) {
this.step = step;
}
}
其中最短路径的那个List数组保存了从起点到终点最短路径所经历的节点。
二、每一个节点的最优前一节点
在迪杰斯特拉算法中我们需要保存从起点开始到每一个节点最短步长,这也是图中需要比较得出的步长,同时我们还需要存储该步长下的前一个节点是哪个,这样我们就可以通过终点一个一个往前推到起点,这样就出来了完整的最优路径。
/**
*@Description:
*/
package com.lulei.distance.bean;
public class PreNode {
private int preNodeNum;// 最优 前一个节点
private int nodeStep;// 最小步长
public PreNode(int preNodeNum, int nodeStep) {
this.preNodeNum = preNodeNum;
this.nodeStep = nodeStep;
}
public int getPreNodeNum() {
return preNodeNum;
}
public void setPreNodeNum(int preNodeNum) {
this.preNodeNum = preNodeNum;
}
public int getNodeStep() {
return nodeStep;
}
public void setNodeStep(int nodeStep) {
this.nodeStep = nodeStep;
}
}
三、迪杰斯特拉算法计算过程中需要关注的变量
从介绍迪杰斯特拉算法的图中,我们知道在计算的过程中我们需要保存起点到各个节点的最短距离、已经计算过的节点、下次需要计算节点队列和图中相邻两个节点的距离。我们通过代码来看下具体的定义:
//key1节点编号,key2节点编号,value为key1到key2的步长
private HashMap> stepLength;
//非独立节点个数
private int nodeNum;
//移除节点
private HashSet outNode;
//起点到各点的步长,key为目的节点,value为到目的节点的步长
private HashMap nodeStep;
//下一次计算的节点
private LinkedList nextNode;
//起点、终点
private int startNode;
private int endNode;
我们这里看下stepLength这个属性,它保存了图中相邻两个节点之间的距离,比如key1=1;key2=3;value=9;这代表的意义就是从节点1到节点3的距离是9。通过这样的存储,我们就需要把图中每两个相邻的点保存到这个类型的map中。
四、属性初始化
在开始计算之前,我们需要对这些属性进行初始化,具体如下:
private void initProperty(int start, int end) {
outNode = new HashSet();
nodeStep = new HashMap();
nextNode = new LinkedList();
nextNode.add(start);
startNode = start;
endNode = end;
}
这一步我们需要把起点添加到下一次需要计算的节点队列中。
五、迪杰斯特拉算法
这一步也就是迪杰斯特拉算法的核心部分,在计算的过程中,我们需要进行如下步骤:
1)判断是否达到终止条件,如果达到终止条件,结束本次算法,如果没有达到,执行下一步;(终止条件:下一次需要计算的节点队列没有数据或已经计算过的节点数等于节点总数)
2)获取下一次计算的节点A;
3)从起点到各节点之间的最短距离map中获取到达A点的最小距离L;
4)获取A节点的可达节点B,计算从起点先到A再到B是否优于已有的其他方式到B,如果优于,则更新B节点,否则不更新;
5)判断B是否是已经移除的节点,如果不是移除的节点,把B添加到下一次需要计算的节点队列中,否则不做操作;
6)判断A节点是否还有除B以外的其他节点,如果有,执行第4)步,否则执行下一步;
7)将A节点从下一次需要计算的节点中移除添加到已经计算过的节点中;
8)执行第一步。
我们来看下具体的代码实现:
private void step() {
if (nextNode == null || nextNode.size()
return;
}
if (outNode.size() == nodeNum) {
return;
}
//获取下一个计算节点
int start = nextNode.removeFirst();
//到达该节点的最小距离
int step = 0;
if (nodeStep.containsKey(start)) {
step = nodeStep.get(start).getNodeStep();
}
//获取该节点可达节点
HashMap nextStep = stepLength.get(start);
Iterator> iter = nextStep.entrySet().iterator();
while (iter.hasNext()) {
Entry entry = iter.next();
Integer key = entry.getKey();
//如果是起点到起点,不计算之间的步长
if (key == startNode) {
continue;
}
//起点到可达节点的距离
Integer value = entry.getValue() + step;
if ((!nextNode.contains(key)) && (!outNode.contains(key))) {
nextNode.add(key);
}
if (nodeStep.containsKey(key)) {
if (value
nodeStep.put(key, new PreNode(start, value));
}
} else {
nodeStep.put(key, new PreNode(start, value));
}
}
//将该节点移除
outNode.add(start);
//计算下一个节点
step();
}
六、组装最短路径返回结果
通过前面的计算,已经计算出了起点到各个节点的最短路径,下面就需要组装起点到终点的最短路径,在计算最短距离下的路径方式,我们需要从终点依次往前推,即到达终点最短距离下的前一个节点是A,到达A节点最短距离下的前一节点是B,直到找到起点终止。
private MinStep changeToMinStep() {
MinStep minStep = new MinStep();
minStep.setMinStep(nodeStep.get(endNode).getNodeStep());
minStep.setReachable(true);
LinkedList step = new LinkedList();
minStep.setStep(step);
int nodeNum = endNode;
step.addFirst(nodeNum);
while (nodeStep.containsKey(nodeNum)) {
int node = nodeStep.get(nodeNum).getPreNodeNum();
step.addFirst(node);
nodeNum = node;
}
return minStep;
}
七、接口定义方法实现
public MinStep getMinStep(int start, int end, final HashMap> stepLength) {
this.stepLength = stepLength;
this.nodeNum = this.stepLength != null ? this.stepLength.size() : 0;
//起点、终点不在目标节点内,返回不可达
if (this.stepLength == null || (!this.stepLength.containsKey(start)) || (!this.stepLength.containsKey(end))) {
return UNREACHABLE;
}
initProperty(start, end);
step();
if (nodeStep.containsKey(end)) {
return changeToMinStep();
}
return UNREACHABLE;
}
八、迪杰斯特拉算法完整代码
/**
*@Description:
*/
package com.lulei.distance.dijkstra;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map.Entry;
import com.lulei.distance.Distance;
import com.lulei.distance.bean.MinStep;
import com.lulei.distance.bean.PreNode;
public class DistanceDijkstraImpl implements Distance{
//key1节点编号,key2节点编号,value为key1到key2的步长
private HashMap> stepLength;
//非独立节点个数
private int nodeNum;
//移除节点
private HashSet outNode;
//起点到各点的步长,key为目的节点,value为到目的节点的步长
private HashMap nodeStep;
//下一次计算的节点
private LinkedList nextNode;
//起点、终点
private int startNode;
private int endNode;
/**
* @param start
* @param end
* @param stepLength
* @return
* @Author:lulei
* @Description: start 到 end 的最短距离
*/
public MinStep getMinStep(int start, int end, final HashMap> stepLength) {
this.stepLength = stepLength;
this.nodeNum = this.stepLength != null ? this.stepLength.size() : 0;
//起点、终点不在目标节点内,返回不可达
if (this.stepLength == null || (!this.stepLength.containsKey(start)) || (!this.stepLength.containsKey(end))) {
return UNREACHABLE;
}
initProperty(start, end);
step();
if (nodeStep.containsKey(end)) {
return changeToMinStep();
}
return UNREACHABLE;
}
/**
* 返回最短距离以及路径
*/
private MinStep changeToMinStep() {
MinStep minStep = new MinStep();
minStep.setMinStep(nodeStep.get(endNode).getNodeStep());
minStep.setReachable(true);
LinkedList step = new LinkedList();
minStep.setStep(step);
int nodeNum = endNode;
step.addFirst(nodeNum);
while (nodeStep.containsKey(nodeNum)) {
int node = nodeStep.get(nodeNum).getPreNodeNum();
step.addFirst(node);
nodeNum = node;
}
return minStep;
}
/**
* @param start
* @Author:lulei
* @Description: 初始化属性
*/
private void initProperty(int start, int end) {
outNode = new HashSet();
nodeStep = new HashMap();
nextNode = new LinkedList();
nextNode.add(start);
startNode = start;
endNode = end;
}
/**
* @param end
* @Author:lulei
* @Description:
*/
private void step() {
if (nextNode == null || nextNode.size()
return;
}
if (outNode.size() == nodeNum) {
return;
}
//获取下一个计算节点
int start = nextNode.removeFirst();
//到达该节点的最小距离
int step = 0;
if (nodeStep.containsKey(start)) {
step = nodeStep.get(start).getNodeStep();
}
//获取该节点可达节点
HashMap nextStep = stepLength.get(start);
Iterator> iter = nextStep.entrySet().iterator();
while (iter.hasNext()) {
Entry entry = iter.next();
Integer key = entry.getKey();
//如果是起点到起点,不计算之间的步长
if (key == startNode) {
continue;
}
//起点到可达节点的距离
Integer value = entry.getValue() + step;
if ((!nextNode.contains(key)) && (!outNode.contains(key))) {
nextNode.add(key);
}
if (nodeStep.containsKey(key)) {
if (value
nodeStep.put(key, new PreNode(start, value));
}
} else {
nodeStep.put(key, new PreNode(start, value));
}
}
//将该节点移除
outNode.add(start);
//计算下一个节点
step();
}
}
代码测试
对于上述代码的测试,我们还是使用我们事例图形中的例子,计算从节点1到节点5的最短距离。
/**
*@Description:
*/
package com.lulei.distance.test;
import java.util.HashMap;
import com.lulei.distance.Distance;
import com.lulei.distance.bean.MinStep;
import com.lulei.distance.dijkstra.DistanceDijkstraImpl;
import com.lulei.util.JsonUtil;
public class DistanceTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
HashMap> stepLength = new HashMap>();
HashMap step1 = new HashMap();
stepLength.put(1, step1);
step1.put(6, 14);
step1.put(3, 9);
step1.put(2, 7);
HashMap step2 = new HashMap();
stepLength.put(2, step2);
step2.put(1, 7);
step2.put(3, 10);
step2.put(4, 15);
HashMap step3 = new HashMap();
stepLength.put(3, step3);
step3.put(1, 9);
step3.put(2, 10);
step3.put(4, 11);
step3.put(6, 2);
HashMap step4 = new HashMap();
stepLength.put(4, step4);
step4.put(2, 15);
step4.put(5, 5);
step4.put(3, 11);
HashMap step5 = new HashMap();
stepLength.put(5, step5);
step5.put(6, 9);
step5.put(4, 5);
HashMap step6 = new HashMap();
stepLength.put(6, step6);
step6.put(1, 14);
step6.put(5, 9);
step6.put(3, 2);
Distance distance = new DistanceDijkstraImpl();
MinStep step = distance.getMinStep(1, 5, stepLength);
System.out.println(JsonUtil.parseJson(step));
}
}
这里组装相邻两个节点之间的距离用了大量的代码,我们看下输出结果:
{"reachable":true,"minStep":20,"step":[1,3,6,5]}
最后的思考
最短路径算法在现实生活中其实有很多的用处,比如迷宫解法、路径规划、路由寻址等等,这些问题看似很复杂,其实只需要做对应的转化后还是可以用最基础的算法解决的。