最短路径

Dijkstra 算法详解:分步讲解

Dijkstra 算法是最短路径的主力。它驱动着你的 GPS、你的网络路由,以及很大一部分编程面试。本指南从核心思想出发,一步步构建到一个完整示例和干净的 Python 代码。

12 分钟阅读 更新时间:2026 年 7 月 适合初学者
Mohammed Islam Hadjoudj
Mohammed Islam Hadjoudj
Expert Operations Research Engineer

什么是 Dijkstra 算法?

Dijkstra 算法由 Edsger W. Dijkstra 于 1959 年提出,它在带权图中找到从单一源点到其余每个节点的最短路径。它对有向图和无向图都适用,但有一个硬性条件:每条边的权必须非负

把图想象成一张路网。节点是路口,边是道路,每个权重是走这条路所需的时间或距离。Dijkstra 算法回答了每个导航应用都会问的问题:从我所在的位置到其他所有地方,最快的路线是什么?它是最短路径算法这一更大家族中的一员,也是图论学习路线图中的常客。

核心思想

Dijkstra 算法是贪心的。它为每个节点保存一个暂定的最短距离,并重复一个简单的动作:

总是访问已知距离最小的未访问节点,然后用它来改进它的邻居。

使其正确的洞见是:因为所有权都非负,一旦你选出最近的未访问节点,之后就不可能有任何路径以更低代价到达它。所以某个节点被选中的那一刻,它的距离就是最终的。这一条保证就是整个算法。

具体来说,算法维护:

改进一个邻居称为松弛:如果经过当前节点的路径比邻居已记录的距离更短,就把它调低。

它如何运作,分步讲解

让我们在这张带权图上从节点 A 运行 Dijkstra。蓝色的边将构成最终的最短路径树,每个节点旁边的数字是它从 A 出发的最终最短距离。

4 2 1 5 8 2 6 3 A B C D E F d=0 d=3 d=2 d=8 d=10 d=13
从 A 出发的最短路径树(蓝色)。灰色的边存在,但从来不是抵达的最便宜方式。

下面是执行过程。每一步我们确定最近的未访问节点(加粗),并松弛它的邻居。 表示"尚未到达"。

访问ABCDEF
开始0
A (0)042
C (2)03210
B (3)0328
D (8)03281014
E (10)03281013
F (13)03281013

注意第三步:访问 C 把 B 从 4 降到了 3,因为路线 A → C → B(2 + 1)胜过直接边 A → B(4)。这就是松弛在起作用。在实时图上看它展开会让这个套路瞬间清晰,你可以在算法可视化工具中做到这一点。

Python 实现

这份干净、可用于面试的版本用 Python 的 heapq 作为优先队列。图是一个邻接表,把每个节点映射到一列 (邻居, 权重) 对。

import heapq

def dijkstra(graph, start):
    # 除源点外,每个节点都从无穷远开始。
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    pq = [(0, start)]  # (目前的距离, 节点)

    while pq:
        dist, node = heapq.heappop(pq)

        # 对已确定节点的一个过时且更长的条目:跳过。
        if dist > distances[node]:
            continue

        for neighbour, weight in graph[node]:
            new_dist = dist + weight
            # 松弛:找到了到达邻居的更便宜路径。
            if new_dist < distances[neighbour]:
                distances[neighbour] = new_dist
                heapq.heappush(pq, (new_dist, neighbour))

    return distances

有两个细节很重要。第一,我们压入一个新条目,而不是原地更新堆,然后用 dist > distances[node] 检查来跳过过时条目。这种"惰性删除"让代码保持简单,也是标准做法。第二,算法天然会计算到所有节点的距离;若想在单一目标处提前停止,一取出它就返回即可。

时间与空间复杂度

开销取决于优先队列。每条边至多触发一次压入,而二叉堆上的每次压入或弹出耗费 O(log V)

优先队列时间何时最佳
二叉堆O((V + E) log V)常用选择,稀疏图
斐波那契堆O(E + V log V)稠密图,理论最优
简单数组O(V²)非常稠密的图

空间为 O(V),用于距离表加上队列。这些上界背后的推理,以及它们在所有图算法之间的比较,参见图算法复杂度指南和一页纸的速查表

Dijkstra 何时失效:负权

贪心的保证完全建立在非负权之上。加入一条负边,整件事就可能崩塌。

假设算法把某个节点定为最终,因为它看起来最近,距离为 5。之后它发现一条看似更长的路线,经过一条 -4 的边,实际上以 3 到达该节点。太晚了:Dijkstra 已经把 5 宣布为最终并继续前进。答案是错的。

要记住的规则:非负权,用 Dijkstra。只要有负权,就用 Bellman-Ford,它反复松弛每条边,还能检测负环。

Dijkstra 与其他算法的对比

Dijkstra 只是众多工具之一。选对哪一个归根结底取决于图。

算法负权?最适合时间
BFS仅无权无权最短路径O(V + E)
Dijkstra非负权O((V + E) log V)
Bellman-Ford负权、环检测O(V · E)
A*单一目标,配启发式O(E) 通常

最近的亲戚是 A* 搜索,也就是 Dijkstra 加上一个把搜索导向单一目标的启发式。而 Dijkstra 本身其实就是把普通队列升级为优先队列的广度优先搜索

现实应用

看 Dijkstra 选择它的下一个节点

当你看到优先队列一次又一次取出最便宜的节点时,它就豁然开朗。在实时图上一步步运行 Dijkstra。

打开算法可视化工具

常见问题

Dijkstra 算法是做什么的?

Dijkstra 算法在带权图中,从单个源节点找到到其余每个节点的最短路径,前提是所有边权都非负。它是路由、地图和网络协议背后的标准方法。

Dijkstra 算法的时间复杂度是多少?

以二叉堆作为优先队列,Dijkstra 算法的时间为 O((V + E) log V),空间为 O(V)。使用斐波那契堆可提升到 O(E + V log V),而用简单数组则为 O(V 的平方),在稠密图上更快。

为什么 Dijkstra 算法在负权下不起作用?

Dijkstra 一旦把某个节点从优先队列中取出,就将其定为最终结果,假设之后不会再出现更便宜的路径。负边会打破这个假设,因为更长的路线仍可能降低总代价。对含负权的图请使用 Bellman-Ford。

Dijkstra 算法和 BFS 是一回事吗?

Dijkstra 是广度优先搜索的推广。BFS 用普通队列,在无权图中找最短路径。Dijkstra 把队列换成最小优先队列,因此总是扩展最近的未访问节点,并能处理带权边。

进一步的学习资源

看见它,而不只是读它

当你看到边界向外扩展的那一刻,Dijkstra 就说得通了。加载一张图,按下播放,跟随最短路径逐渐成形。

使用算法可视化工具进行练习