迪杰斯特拉(Dijkstra)算法详解与数据结构实现
一、引言
在图论中,最短路径问题是核心应用场景之一——从地图导航、网络路由优化到物流路径规划,都需要高效的算法找到两点间的最优路径。迪杰斯特拉(Dijkstra)算法作为解决带非负权重图最短路径问题的经典算法,凭借“贪心策略+松弛操作”的核心逻辑,成为计算机科学领域的基础知识点。
本文将结合完整Java代码,从算法原理、三大核心数据结构(图、三元记录表、优先队列)、代码实现细节到实际应用,层层拆解迪杰斯特拉算法,帮助读者不仅“会用”,更能“理解为什么这么用”。
二、迪杰斯特拉算法核心原理
2.1 算法适用场景
- 无向图或有向图(本文以无向图为例)
- 边权重非负(若存在负权重边,需使用贝尔曼-福特算法)
- 单源最短路径(从一个起点出发,找到到所有其他节点的最短路径)
2.2 核心思想
迪杰斯特拉算法的本质是贪心策略:每次选择“当前已确定的最短路径节点”作为中间节点,更新其邻居节点的最短路径(即“松弛操作”),直到所有节点的最短路径都被确定。
2.3 算法步骤(对应视频可视化流程)
初始化:
- 起点到自身的最短距离设为0,到其他所有节点的距离设为“无穷大”(表示暂未可达)。
- 用优先队列存储待处理的节点,按当前最短距离升序排序(小顶堆)。
- 用三元记录表记录每个节点的“名称、当前最短距离、前驱节点”(前驱节点用于后续路径回溯)。
主循环(队列非空时):
- 弹出优先队列中“当前最短距离最小”的节点(贪心选择)。
- 遍历该节点的所有邻居节点,计算“起点→当前节点→邻居节点”的路径总代价。
- 松弛操作:若新路径代价小于邻居节点当前记录的最短距离,则更新邻居节点的最短距离和前驱节点,并调整优先队列中邻居节点的优先级(Decrease Key操作)。
终止条件:
- 优先队列为空(所有节点均已处理),或目标节点已被弹出队列(单目标最短路径场景)。
路径回溯:
- 从目标节点出发,通过三元记录表中的“前驱节点”反向遍历,直到回到起点,再反转路径即为“起点→目标节点”的最短路径。
2.4 关键概念解释
- 贪心选择:每次选择当前代价最小的节点,确保该节点的最短路径已被永久确定(因边权重非负,后续不会出现更短路径)。
- 松弛操作:对边
u→v,若dist[v] > dist[u] + weight(u,v),则更新dist[v] = dist[u] + weight(u,v),本质是“发现更优路径”的过程。 - Decrease Key:当邻居节点的最短距离被更新后,需要调整其在优先队列中的优先级(使其提前被处理),是算法高效性的关键。
三、三大核心数据结构设计与实现
迪杰斯特拉算法的高效运行依赖三个核心数据结构,以下结合Java代码逐一解析其设计思路和作用。
3.1 图(Graph):存储节点与边的关系
3.1.1 数据结构选择:邻接表
图的存储方式有两种:邻接矩阵和邻接表。本文选择邻接表,原因如下:
- 空间效率高:对于稀疏图(边数远小于节点数的平方),邻接表仅存储实际存在的边,避免邻接矩阵的大量冗余。
- 遍历高效:获取某个节点的所有邻居时,直接遍历对应列表即可,时间复杂度
O(degree(u))(degree(u)为节点u的度数)。
3.1.2 代码实现解析
1 | static class Graph { |
3.1.3 核心API说明
| 方法名 | 作用 | 复杂度 |
|---|---|---|
Graph(int n) |
初始化包含n个节点的空图 | O(n) |
addEdge(...) |
向图中添加一条边(无向/有向) | O(1) |
getNeighbors(n) |
获取节点n的所有邻居边列表 | O(1) |
3.2 三元记录表(NodeRecord):路径信息的“账本”
3.2.2 设计目的
算法执行过程中,需要记录每个节点的三个关键信息(对应视频中的“路径表格”):
- 节点名称(用于直观展示,如A、B、C);
- 起点到该节点的当前最短距离(动态更新);
- 前驱节点(即“通过哪个节点到达当前节点”,用于最终路径回溯)。
三元记录表是算法的“核心账本”,所有松弛操作、路径计算都依赖它的数据。
3.2.3 代码实现解析
1 | static class NodeRecord { |
3.2.4 关键细节
- 无穷大初始化:用
Integer.MAX_VALUE表示“暂未可达”,但需注意避免整数溢出(若边权重较大,可改用Long.MAX_VALUE)。 - update方法:仅在“新路径更短”时调用,是“松弛操作”的直接体现。
3.3 优先队列(PriorityQueueWithDecreaseKey):贪心选择的“调度器”
3.3.1 设计目的
算法的“贪心选择”依赖一个高效的调度器:每次从“未确定最短路径的节点”中,快速选出“当前最短距离最小”的节点。优先队列(小顶堆)是最优选择,能在O(log n)时间内完成“弹出最小值”和“插入元素”操作。
3.3.2 核心挑战:Decrease Key操作
当某个节点的最短距离通过松弛操作被更新(变小)时,需要调整它在优先队列中的优先级(使其提前被处理)——这就是Decrease Key操作。
Java默认的PriorityQueue不支持直接更新队列中元素的优先级,本文采用“重新入队”的方式模拟(简单高效,适合学习场景),生产环境可优化为“自定义优先队列+索引映射”。
3.3.3 代码实现解析
1 | static class PriorityQueueWithDecreaseKey { |
3.3.4 核心逻辑说明
- 排序规则:通过
Comparator.comparingInt(node -> records[node].shortestDistance)实现“按节点当前最短距离升序排序”,确保每次弹出的是代价最小的节点。 - isProcessed数组:避免重复处理已确定最短路径的节点(因“重新入队”可能导致队列中存在同一节点的多个副本,弹出一个后标记为已处理,其余副本无需再处理)。
- Decrease Key模拟:当节点距离被更新后,重新加入队列,新副本的优先级会高于旧副本(因距离更小),从而实现“提前调度”。
四、迪杰斯特拉算法完整流程(结合代码)
4.1 算法核心逻辑代码
1 | public static void dijkstra(Graph graph, NodeRecord[] records, int startNode) { |
4.2 步骤拆解(对应代码行)
| 步骤 | 代码位置 | 核心动作 |
|---|---|---|
| 初始化 | 第3行 | 优先队列绑定三元记录表,起点距离设为0并入队 |
| 贪心选择 | 第7行 | 弹出队列中距离最小的节点,标记为已处理 |
| 遍历邻居 | 第10行 | 通过图的邻接表获取当前节点的所有邻居 |
| 跳过已处理 | 第15行 | 避免重复处理已确定最短路径的节点 |
| 计算新代价 | 第19-20行 | 计算“当前节点路径 + 边权重”的新距离 |
| 松弛操作 | 第23-25行 | 新路径更短时,更新三元记录表并调整队列优先级 |
| 终止 | 第5行 | 队列为空,所有节点的最短路径已确定 |
4.3 路径回溯:从“账本”到“路径”
算法执行完成后,三元记录表中存储了每个节点的前驱节点,通过回溯即可得到最短路径:
1 | public static List<String> getShortestPath(NodeRecord[] records, int targetNode) { |
示例:若目标节点C的前驱是E,E的前驱是B,B的前驱是A,则回溯过程为C→E→B→A,反转后得到最短路径A→B→E→C。
五、测试用例与结果分析
5.1 测试图构建(还原视频示例)
视频中使用7节点无向图,节点映射为:0=A, 1=B, 2=C, 3=D, 4=E, 5=F, 6=G,边权重如下:
1 | // 构建图 |
5.2 图的样式
graph LR
A[0:A] ---|3| F[5:F]
A[0:A] ---|2| B[1:B]
A[0:A] ---|5| D[3:D]
B[1:B] ---|6| F[5:F]
B[1:B] ---|3| E[4:E]
B[1:B] ---|9| C[2:C]
E[4:E] ---|6| G[6:G]
E[4:E] ---|1| D[3:D]
E[4:E] ---|2| C[2:C]
5.3 运行结果
1 | === 从起点 A 到各节点的最短路径 === |
5.4 结果验证
- 贪心策略体现:起点A的邻居中,B的距离(2)小于F(3)和D(5),因此优先处理B;
- 松弛操作体现:处理B时,更新E的距离为2+3=5;处理E时,更新C的距离为5+2=7(优于直接A→B→C的9);
- 路径回溯体现:C的前驱是E,E的前驱是B,B的前驱是A,最终路径正确。
六、时间复杂度与优化方向
6.1 时间复杂度分析
假设图中有n个节点、m条边:
- 优先队列操作:每个边最多导致一次
decreaseKey(重新入队),队列中最多有m个元素,每次入队/出队的时间复杂度为O(log m); - 遍历所有边:
O(m); - 总时间复杂度:
O(m log m)(适合稀疏图)。
若使用优化后的优先队列(支持O(1)索引查找),时间复杂度可优化为O(m log n)。
6.2 优化方向
- Decrease Key优化:使用
TreeSet或自定义优先队列(结合节点索引映射),避免重复入队,减少冗余操作; - 空间优化:对于大规模图,可使用
HashMap存储邻接表(无需提前指定节点数); - 处理负权重:迪杰斯特拉算法不支持负权重边,若需处理,可改用贝尔曼-福特算法或SPFA算法;
- 单目标最短路径:当仅需查找起点到某个目标节点的路径时,可在目标节点被弹出队列时直接终止算法(无需处理所有节点)。
七、总结
迪杰斯特拉算法的核心是“贪心策略+松弛操作”,而三大数据结构则是算法落地的关键:
- 图(邻接表):高效存储节点与边的关系,支撑邻居遍历;
- 三元记录表:动态记录路径信息,是算法的“数据核心”;
- 优先队列:实现贪心选择,是算法的“调度核心”。
完整代码
1 | import java.util.*; |
通过本文的讲解,读者可清晰理解算法与数据结构的对应关系——算法提供逻辑思路,数据结构提供实现载体。掌握这三者的协同工作原理,不仅能灵活运用迪杰斯特拉算法,更能提升“根据问题选择合适数据结构”的思维能力。