Menu
CoddyTech

Dijkstra 算法

最后更新

Dijkstra 算法在边权非负的图中,求出从源节点到其他每个节点的最短路径。它为每个节点维护一个暂定距离,反复从未确定的节点中选出暂定距离最小的那个并将其确定下来,然后松弛它的边:只要经过当前节点能找到更短的路线,就更新邻居的距离。点击上方的“播放”,观察每确定一个节点,距离如何随之下降。

关键思想是贪心:一旦选出最近的未确定节点,它的距离就是最终结果,因为通往它的其他路线都必须经过一个本来就更远的节点。使用二叉堆实现的优先队列时,Dijkstra 的运行时间是 O((V + E) log V)。它要求权重非负,如果边可能为负,请使用 Bellman-Ford。

时间和空间复杂度

实现方式复杂度说明
二叉堆O((V + E) log V)常用且实用的选择
数组扫描O(V²)更简单,适合稠密图
空间O(V)距离数组加优先队列
前提条件权重非负负权边会破坏贪心选择

分步说明

步骤发生了什么
1把源节点的距离设为 0,其他节点都设为无穷大。
2选出暂定距离最小的未确定节点。
3把它标记为已确定,它的最短距离现在是最终结果。
4对每个邻居,计算经过当前节点的距离加上边的权重。
5如果结果比邻居当前的距离小,就松弛它。
6重复,直到所有可达节点都已确定。

示例演算

在边为 A-B=4、A-C=1、C-B=2、C-D=5、B-D=1 的图上,求从源节点 A 出发的最短路径:

步骤确定距离操作
0-A=0, B=∞, C=∞, D=∞初始化:源节点 A=0,其余都是无穷大。
1A (0)B=4, C=1, D=∞松弛从 A 出发的边:令 B=4、C=1。
2C (1)B=3, D=6经过 C:B=1+2=3 优于 4;D=1+5=6。
3B (3)D=4经过 B:D=3+1=4 优于 6。
4D (4)A=0, C=1, B=3, D=4确定 D,没有可松弛的边了,结束。

什么时候使用 Dijkstra 算法

适合使用的情况不适合使用的情况
所有边的权重都非负有边可能为负,应使用 Bellman-Ford
需要从一个源点到所有节点的最短路径需要所有点对之间的最短路径,Floyd-Warshall 更简单
图带有权重,并且需要精确的距离图没有权重,普通 BFS 更快也更简单
手头有好用的堆或优先队列想借助启发式快速到达单个目标,应使用 A*

Dijkstra's Algorithm 代码

简洁、可运行的 Dijkstra's Algorithm 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Dijkstra's Algorithm 的 Python 代码

Python
1import heapq2
3
4def dijkstra(graph, start):5    dist = {node: float("inf") for node in graph}6    dist[start] = 07    pq = [(0, start)]8    while pq:9        d, node = heapq.heappop(pq)10        if d > dist[node]:11            continue  # stale entry, a shorter path was already found12        for neighbor, weight in graph[node]:13            new_dist = d + weight14            if new_dist < dist[neighbor]:15                dist[neighbor] = new_dist16                heapq.heappush(pq, (new_dist, neighbor))17    return dist18
19
20graph = {21    "A": [("B", 4), ("C", 1)],22    "B": [("D", 1)],23    "C": [("B", 2), ("D", 5)],24    "D": [("E", 3)],25    "E": [],26}27
28for node, d in dijkstra(graph, "A").items():29    print(f"A -> {node}: {d}")
在 Python 在线编辑器中运行这段代码

Dijkstra 算法常见问题

Dijkstra 算法的时间复杂度是多少?
使用二叉堆实现的优先队列时,运行时间是 O((V + E) log V)。每一步都扫描数组找最小值的简单版本是 O(V²),在稠密图上反而可能更快。两者都占用 O(V) 的空间。
为什么 Dijkstra 不能处理负权边?

Dijkstra 假设一旦确定了最近的未确定节点,这个距离就是最终结果。负权边可能在之后为一个已确定的节点产生更短的路径,从而打破这个假设。对于有负权边的图,请使用 Bellman-Ford 算法。

Dijkstra 和 BFS 有什么区别?

BFS 用普通队列,按边数求最短路径(相当于每条边的权重都是 1)。Dijkstra 把这个思路推广到带权图:借助优先队列,始终扩展总距离最小的节点。在无权图上,两者得到的路径相同。

Dijkstra 和 A* 搜索有什么区别?

A* 就是 Dijkstra 加上一个估算到目标剩余距离的启发函数,所以它会把搜索引向目标,而不是向所有方向均匀扩展。当启发函数为零时,A* 就退化成 Dijkstra。如果只有一个终点,并且有好的可采纳启发函数,就用 A*;如果需要到每个节点的距离,就用 Dijkstra。

什么时候该用 Dijkstra 而不是 Bellman-Ford?
只要所有边的权重都非负,就用 Dijkstra,它的 O((V + E) log V) 比 Bellman-Ford 的 O(V·E) 更快。只有当边可能为负,或者需要检测负权回路时,才选择 Bellman-Ford。在非负权图上,Dijkstra 几乎总是更好的选择。
节点确定之后,Dijkstra 还会再访问它吗?

不会。节点一旦确定,它的距离就是最终结果,不会再被松弛。堆实现中一个常见的坑是:节点的距离变小后,优先队列里还留着过时的条目。如果弹出的节点已经确定(弹出的距离大于记录的距离),就必须跳过它。忘了这个检查,结果仍然正确,但会做很多无用功。

Coddy 编程语言插图

在 Coddy 掌握算法

开始