请看如下的示例图,该图有 V1-V7 七个顶点,每个顶点之间的距离、路径走向如图所示:
假设这是一幅地图,我们很多时候都需要搜路径,比如从家到公司的路线图(也就是说,家是一个点,公司是另一个点)。上图的各个点可以想象成分岔路口。当然,如果你不在分岔路口,你在某条路上,也可以理解为你在一个有两条路的分岔路口上,也就是向前行或者向后退。由于有单行道的存在,所以有的路是只能单向通行的(本示例图全部为单行道)。现在问题来了,假设上面的示例图是一个地图,你的家在V1点,你的公司在V6点,你走什么样的路线,才能走最短的路径到公司呢?有没有一个算法,可以求出从你家V1为起点,到所有其他点的最短路径呢?
这就是图论里面的最短路径算法,由于每一条通路都是单向的,所以又叫单源最短路径问题。解决单源最短路径问题的一般方法叫做Dijkstra算法。这个有30年历史的解法是贪婪算法的最好例子。话不多说,精髓全在代码和其间的注释里,可复制到IDE中运行:
1、图的顶点类,包含一到多个边:
import java.util.HashSet;
import java.util.Set;
/**
* 图的顶点类
* @Author: LiYang
* @Date: 2019/9/8 21:32
*/
public class Vertex {
//顶点的数据
public String value;
//顶点的边的集合(其相邻的顶点和权重)
public Set<Edge> neighbors = new HashSet<>();
/**
* 空构造方法
*/
public Vertex(){
}
/**
* 构造方法,输入顶点值
* @param value 顶点值
*/
public Vertex(String value) {
this.value = value;
}
/**
* 为该顶点加入边
* @param neighbor 边的另一顶点
* @param weight 边的权重
*/
public void addEdge(Vertex neighbor, int weight){
neighbors.add(new Edge(neighbor, weight));
}
/**
* 重新toString()方法,打印顶点的值
* @return
*/
@Override
public String toString() {
return value;
}
}
2、图的边类,包含目的顶点,以及权重(距离):
/**
* 图的边类,带权,本图例是单向边
* Edge是属于某个顶点的,所以出发点是该顶点
* Edge属性只有目的点,以及权重
* @Author: LiYang
* @Date: 2019/9/8 21:32
*/
public class Edge {
//边的终点(邻居)
public Vertex neighbor;
//权重(距离)
public int weight;
/**
* 无参构造
*/
public Edge(){
}
/**
* 全参数构造
* @param neighbor 邻接点
* @param weight 权重
*/
public Edge(Vertex neighbor, int weight) {
this.neighbor = neighbor;
this.weight = weight;
}
}
3、迪杰斯特拉(Dijkstra)算法表类,用于动态统计路径总距离和顶点关系的表
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 迪杰斯特拉(Dijkstra)算法表
* @Author: LiYang
* @Date: 2019/9/11 21:26
*/
public class DijkstraTable {
//顶点
public Vertex vertex;
//是否已访问
public boolean known;
//总距离
public int distance;
//上一顶点
public Vertex previous;
/**
* 初始化迪杰斯特拉(Dijkstra)算法表
* @param vList 顶点集合
* @return 迪杰斯特拉(Dijkstra)算法表
*/
public static Map<String, DijkstraTable> initialDijkstraTable(List<Vertex> vList){
//最后返回的迪杰斯特拉(Dijkstra)算法表
Map<String, DijkstraTable> dijkstraTableMap = new HashMap<>();
//开始循环添加
for (Vertex vertex : vList){
//当前节点对应的迪杰斯特拉(Dijkstra)算法表的行
DijkstraTable dijkstraTable = new DijkstraTable();
//顶点
dijkstraTable.vertex = vertex;
//初始化为未访问
dijkstraTable.known =false;
//初始化距离为无穷大
dijkstraTable.distance = Integer.MAX_VALUE;
//上一顶点初始化置空
dijkstraTable.previous = null;
//以顶点名为键,放入Map中
dijkstraTableMap.put(vertex.value, dijkstraTable);
}
//返回初始化好了的迪杰斯特拉(Dijkstra)算法表
return dijkstraTableMap;
}
/**
* 寻找迪杰斯特拉(Dijkstra)算法表中距离最小的value的key
* @param dijkstraTableMap 待查找的迪杰斯特拉(Dijkstra)算法表
* @return 迪杰斯特拉(Dijkstra)算法表最小距离的key
*/
public static String getMinimumDijkstraTableKey(Map<String, DijkstraTable> dijkstraTableMap){
//最小距离
int minimunDistance = Integer.MAX_VALUE;
//第一次遍历普利姆(Prim)算法表,找出最小值
for (Map.Entry<String, DijkstraTable> item : dijkstraTableMap.entrySet()){
//如果已经访问过,则跳过
if (item.getValue().known){
continue;
}
//得到当前的距离
int currentDistance = item.getValue().distance;
//更新最小值
if (minimunDistance > currentDistance){
minimunDistance = currentDistance;
}
}
//根据找到的最小值,找到最小值的key
for (Map.Entry<String, DijkstraTable> item : dijkstraTableMap.entrySet()){
//如果已经访问过,则跳过
if (item.getValue().known){
continue;
}
//得到当前的距离
int currentDistance = item.getValue().distance;
//如果当前距离就是最小距离,则返回其key
if (currentDistance == minimunDistance){
return item.getKey();
}
}
//如果找不到,则表示迪杰斯特拉(Dijkstra)算法终止
//因为算法while循环中加入了判断,所以不会到这里
return "DijkstraFinished";
}
/**
* 重新toString()方法,方便调试查看数据
* @return
*/
@Override
public String toString() {
return "DijkstraTable{" +
"vertex=" + vertex +
", known=" + known +
", distance=" + distance +
", previous=" + previous +
'}';
}
}
4、图类,里面包含创建上面示例图的方法,以及迪杰斯特拉(Dijkstra)算法求最短路径的方法,还有运行的main方法:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 数据结构与算法之:图
* @Author: LiYang
* @Date: 2019/9/11 21:42
*/
public class Graph {
/**
* 构建迪杰斯特拉(Dijkstra)算法所需要的图(本文刚开始的示例图)
* 注意,迪杰斯特拉(Dijkstra)算法需要连通图,本例是有向连通图
* 如果无向图,就双方都addEdge,有向图,就双方或单方addEdge
* @return 图的顶点集合
*/
public static List<Vertex> buildDijkstraGraph(){
//创建顶点
Vertex vertex1 = new Vertex("V1");
Vertex vertex2 = new Vertex("V2");
Vertex vertex3 = new Vertex("V3");
Vertex vertex4 = new Vertex("V4");
Vertex vertex5 = new Vertex("V5");
Vertex vertex6 = new Vertex("V6");
Vertex vertex7 = new Vertex("V7");
//为顶点创建边
//注意,本算法用到的图,是有向图,
//两个顶点间并不一定互通
vertex1.addEdge(vertex2, 2);
vertex1.addEdge(vertex4, 1);
vertex2.addEdge(vertex5, 10);
vertex2.addEdge(vertex4, 3);
vertex3.addEdge(vertex1, 4);
vertex3.addEdge(vertex6, 5);
vertex4.addEdge(vertex3, 2);
vertex4.addEdge(vertex5, 2);
vertex4.addEdge(vertex6, 8);
vertex4.addEdge(vertex7, 4);
vertex5.addEdge(vertex7, 6);
//因为图中V6顶点只有到达它的边,它没有出去的边
//所以,这里vertex6就不addEdge了
vertex7.addEdge(vertex6, 1);
//所有顶点的集合
List<Vertex> vList = new ArrayList<>();
vList.add(vertex1);
vList.add(vertex2);
vList.add(vertex3);
vList.add(vertex4);
vList.add(vertex5);
vList.add(vertex6);
vList.add(vertex7);
//返回所有顶点
return vList;
}
/**
* 迪杰斯特拉(Dijkstra)算法,求最短路径
* @param vList 图的顶点的集合
*/
public static void dijkstraAlgorithm(List<Vertex> vList){
//先生成该顶点集合的迪杰斯特拉(Dijkstra)算法表
Map<String, DijkstraTable> dijkstraTable = DijkstraTable.initialDijkstraTable(vList);
//我们求第一个顶点到每一个顶点的最短路径,所以我们先把
//第一个迪杰斯特拉(Dijkstra)算法表的第一个顶点的距离设为0
//注意,如果想要求出所有顶点到其他顶点的最短路径,复用这个算法,
//然后将下面的 "vList.get(0).value" 中的 "0" 循环到vList.size()-1即可
dijkstraTable.get(vList.get(0).value).distance = 0;
//重要:最短路径算法之迪杰斯特拉(Dijkstra)算法
while (isNotAllKnown(dijkstraTable)){
//寻找迪杰斯特拉(Dijkstra)算法表中最小距离的key(known为false中寻找最小)
String minimumKey = DijkstraTable.getMinimumDijkstraTableKey(dijkstraTable);
//先标记该顶点为known
dijkstraTable.get(minimumKey).known = true;
//获得该顶点
Vertex currentVertex = dijkstraTable.get(minimumKey).vertex;
//访问该顶点的所有能够到达的相邻的邻居
for (Edge item : currentVertex.neighbors){
//获得当前邻居的value
String neighborKey = item.neighbor.value;
//如果该邻居顶点已被访问,则跳过
if (dijkstraTable.get(neighborKey).known){
continue;
}
//如果该邻居顶点没有被访问,则先拿出迪杰斯特拉(Dijkstra)算法表里面的距离
int dijkstraDistance = dijkstraTable.get(minimumKey).distance;
//拿出到邻居路径的权重
int neighborDistance = item.weight;
//如果当前顶点的距离 + 到邻居的权重小于迪杰斯特拉(Dijkstra)算法表里面的距离
if (dijkstraDistance + neighborDistance < dijkstraTable.get(neighborKey).distance){
//需要更新邻居迪杰斯特拉(Dijkstra)算法表里面的距离
dijkstraTable.get(neighborKey).distance = dijkstraDistance + neighborDistance;
//然后迪杰斯特拉(Dijkstra)算法表里面的上一顶点,更新为当前顶点
dijkstraTable.get(neighborKey).previous = currentVertex;
}
}
}
//上面的while循环结束后,迪杰斯特拉(Dijkstra)算法即终止,解读最终生成的
//迪杰斯特拉(Dijkstra)算法表,循环打印第一个顶点到每个顶点的最短路径
for (Vertex item : vList){
printShortestRouteByDestinationVertex(dijkstraTable, item.value);
}
}
/**
* 检查图的所有顶点是否全部已访问
* @param dijkstraTable 待检查的迪杰斯特拉(Dijkstra)算法表
* @return 是否不是全部已访问
*/
public static boolean isNotAllKnown(Map<String, DijkstraTable> dijkstraTable){
//将迪杰斯特拉(Dijkstra)算法表遍历
for (Map.Entry<String, DijkstraTable> item : dijkstraTable.entrySet()){
//如果找到了一个known为false的点
if (!item.getValue().known){
//证明不是全known,返回true
return true;
}
}
//遍历完了,还没有找到known为false的点,则证明全部known
return false;
}
/**
* 根据最终生成的迪杰斯特拉(Dijkstra)算法表,算出起点到当前目的地顶点的最短路径
* @param dijkstraTable 最终生成的迪杰斯特拉(Dijkstra)算法表
* @param destinationKey 目的地顶点的key
*/
public static void printShortestRouteByDestinationVertex(Map<String, DijkstraTable> dijkstraTable, String destinationKey){
//用于存储路径的集合
List<String> shortestRoute = new ArrayList<>();
//将目的地节点装入路径图,从目的地开始倒推到起点
shortestRoute.add(destinationKey);
//将目的地节点,作为当前起点
String currentVertexKey = destinationKey;
//如果当前节点还有上一个节点
while (dijkstraTable.get(currentVertexKey).previous != null){
//获取上一节点
String previousKey = dijkstraTable.get(currentVertexKey).previous.value;
//将上一节点装入路径图
shortestRoute.add(previousKey);
//上一节点变成当前节点,供下一次循环使用
currentVertexKey = previousKey;
}
//因为我们是反向求路径的,所以将路径图逆序
Collections.reverse(shortestRoute);
//打印最短路径图标题
System.out.print(shortestRoute.get(0) + " 到 "+ shortestRoute.get(shortestRoute.size()-1) + " 的最短路径:");
//打印最短路径图
for (int i = 0; i < shortestRoute.size(); i++) {
if (i == 0){
System.out.print(shortestRoute.get(i));
} else {
System.out.print(" -> " + shortestRoute.get(i));
}
//换行
if (i == shortestRoute.size() - 1){
System.out.println();
}
}
}
/**
* 运行迪杰斯特拉(Dijkstra)算法,求出示例图第一个顶点到所有顶点的的最短路径
* @param args
*/
public static void main(String[] args) {
//构建示例图
List<Vertex> vList = buildDijkstraGraph();
//运行迪杰斯特拉(Dijkstra)算法,需要在控制台查看打印结果
dijkstraAlgorithm(vList);
}
}
5、运行main方法,控制台输出示例图中V1顶点到其他顶点的最短路径的路线图
V1 到 V1 的最短路径:V1
V1 到 V2 的最短路径:V1 -> V2
V1 到 V3 的最短路径:V1 -> V4 -> V3
V1 到 V4 的最短路径:V1 -> V4
V1 到 V5 的最短路径:V1 -> V4 -> V5
V1 到 V6 的最短路径:V1 -> V4 -> V7 -> V6
V1 到 V7 的最短路径:V1 -> V4 -> V7
6、我们回顾之前的示例图,根据上面给出的最短路径路线图,可以看出程序算出的路径确实是最短的路径,你找不到比上面的路径方案还要短的路径了: