一、Dijkstra 算法简介
Dijkstra(迪杰斯特拉)算法是图论中最经典的单源最短路径算法,用于计算一个源节点到图中所有其他节点的最短路径长度。它由荷兰计算机科学家艾兹格·迪杰斯特拉于1956年提出,广泛用于路由选择、导航系统、网络通信等领域。
本代码使用邻接表存储图结构,配合优先队列(PriorityQueue)实现,时间复杂度为 O(E log V),其中 E 是边数、V 是顶点数,适合处理节点较多但边相对稀疏的图。
需要注意的是,Dijkstra 算法要求图中不存在负权边。若存在负权边,应改用 Bellman-Ford 算法或 SPFA。
二、核心概念
理解本代码前,需要先掌握以下几个关键概念:
-
邻接表:用 Map<Integer, List<Edge>> 存储图,每个顶点对应其出边列表
-
边(Edge):包含终点to和权重weight两个属性
-
优先队列:每次取出当前距离最小的节点,保证贪心选择的正确性
-
dist数组:记录从起点到各节点的当前最短距离,初始为无穷大
-
松弛操作:若通过当前节点能获得更短路径,则更新dist并加入队列
-
visited数组:标记节点是否已确定最短路径,避免重复处理
Dijkstra 的核心思想是贪心:每次从未处理的节点中选出距离起点最近的节点,确定其最短路径,然后通过它松弛其邻居节点。
三、代码整体架构
本代码由 Edge 边类、Node 节点类(实现 Comparable 接口用于优先队列排序)、dijkstra 核心算法和 main 主函数四部分组成。main 函数构造了一个包含0、1、2、3四个节点的无向图,计算从节点0出发到所有节点的最短距离并打印。
四、核心代码逐段解析
4.1 Edge 边类
Edge 类表示一条有向边,包含两个成员:to 表示边的终点顶点编号,weight 表示边的权重。在邻接表中,每个顶点对应的 List<Edge> 存储了从该顶点出发的所有边。
4.2 Node 节点类与优先队列
Node 类封装了顶点编号 vertex 和该顶点到起点的当前距离 dist,并实现 Comparable 接口。compareTo 方法按 dist 升序比较,这样 PriorityQueue 每次弹出(poll)的就是当前距离最小的节点,这正是 Dijkstra 贪心策略的关键。
4.3 dijkstra 核心算法
算法初始化 dist 数组为无穷大(Integer.MAX_VALUE),将起点距离设为0并加入优先队列。主循环中每次弹出距离最小的节点,若已访问则跳过,否则标记为已访问,并遍历其所有出边进行松弛:如果经过当前节点到达邻居的距离更短,就更新 dist 并重新加入队列。循环结束后,dist 数组即为起点到各节点的最短距离。
4.4 main 函数构建图与输出
main 函数通过 HashMap 构建邻接表表示的图,节点0、1、2、3之间由多条带权边相连,构成一个无向图。调用 dijkstra(graph, 0, 4) 计算从节点0出发到4个节点的最短距离,并按格式打印结果。
五、完整源代码
以下是完整的可运行代码,可直接复制编译运行。
java
import java.util.*;
public class Dijkstra {
static class Edge {
int to, weight;
Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
}
static class Node implements Comparable<Node> {
int vertex, dist;
Node(int v, int d) {
vertex = v;
dist = d;
}
@Override
public int compareTo(Node other) {
return Integer.compare(this.dist, other.dist);
}
}
public static int[] dijkstra(Map<Integer, List<Edge>> graph, int start, int n) {
int[] dist = new int[n];
int[] parent = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
Arrays.fill(parent, -1);
dist[start] = 0;
PriorityQueue<Node> pq = new PriorityQueue<>();
pq.offer(new Node(start, 0));
boolean[] visited = new boolean[n];
while (!pq.isEmpty()) {
Node cur = pq.poll();
if (visited[cur.vertex]) continue;
visited[cur.vertex] = true;
for (Edge e : graph.getOrDefault(cur.vertex, new ArrayList<>())) {
int newDist = cur.dist + e.weight;
if (newDist < dist[e.to]) {
dist[e.to] = newDist;
parent[e.to] = cur.vertex;
pq.offer(new Node(e.to, newDist));
}
}
}
return dist;
}
public static void main(String[] args) {
Map<Integer, List<Edge>> graph = new HashMap<>();
graph.put(0, Arrays.asList(new Edge(1, 1), new Edge(2, 4)));
graph.put(1, Arrays.asList(new Edge(0, 1), new Edge(2, 2), new Edge(3, 5)));
graph.put(2, Arrays.asList(new Edge(0, 4), new Edge(1, 2), new Edge(3, 1)));
graph.put(3, Arrays.asList(new Edge(1, 5), new Edge(2, 1)));
int[] dist = dijkstra(graph, 0, 4);
System.out.println("最短距离:");
for (int i = 0; i < dist.length; i++) {
System.out.printf(" 0 → %d: %d%n", i, dist[i]);
}
}
}
六、运行结果
在本机 JDK 11 环境实际编译运行,输出如下:
java
最短距离:
0 → 0: 0
0 → 1: 1
0 → 2: 3
0 → 3: 4
从结果可以看出:从节点0出发,到自身距离为0;到节点1的最短路径为0→1(权重1);到节点2的最短路径为0→1→2(1+2=3,而非直接0→2的4);到节点3的最短路径为0→1→2→3(1+2+1=4,而非0→1→3的6)。结果验证了算法能够正确找到经过中转的更短路径,符合Dijkstra算法预期。
七、编译运行方法
将代码保存为 Dijkstra.java,在命令行编译运行:
java
javac Dijkstra.java
java Dijkstra
也可以直接在 IntelliJ IDEA / Eclipse 中新建类,粘贴代码后运行 main 方法。需要 JDK 8 及以上版本。
八、注意事项与知识点总结
下表汇总了本实现的关键知识点与常见注意事项:
|---------------|-------------------------------------|
| 要点 | 说明 |
| 适用范围 | 求单源最短路径,要求图中无负权边 |
| 贪心策略 | 每次选出距离最小的节点确定其最短路径 |
| 优先队列 | Node实现Comparable按距离排序,poll取出最小值 |
| 松弛操作 | 发现更短路径时更新dist并重新入队 |
| visited标记 | 标记已确定最短路径的节点,避免重复处理 |
| 邻接表存储 | Map<顶点, List<Edge>>,适合稀疏图 |
| 时间复杂度 | O(E log V),E为边数、V为顶点数 |
九、拓展方向
本文实现了基于邻接表和优先队列的Dijkstra算法,后续可以在此基础上进一步扩展:
-
输出最短路径本身(利用parent数组回溯路径节点)
-
使用优先队列保存完整的路径信息,支持路径还原
-
对比Floyd-Warshall全源最短路径算法
-
在负权边场景改用Bellman-Ford或SPFA算法
-
扩展到最小生成树算法(Prim与Dijkstra思想相近)