【算法图解】最短路径:Dijkstra 算法
Dijkstra 解决“非负权图”的单源最短路径问题。本文用“每次确定一个最短点”的贪心直觉讲清楚它为什么正确,并给出基于优先队列的实现、复杂度、常见坑与和 BFS 的边界区分。
20 分钟阅读
小明
【算法图解】最短路径:Dijkstra 算法
BFS 能解决无权图最短路径。
那如果边有权重呢?
- 道路长度不同
- 成本不同
- 延迟不同
只要权重都是非负数,Dijkstra 就是你最常用的答案。
这篇文章让你搞清楚:
- 它的核心直觉是什么
- 为什么每次“定点”是安全的
- 工程实现怎么写(优先队列)
- 哪些情况不能用(负权边)
1. 问题定义
给定一个带权图 $G=(V,E)$,权重 $w(u,v) \ge 0$。
求从起点 $s$ 到所有点的最短距离 $distv$。
2. Dijkstra 的核心直觉:每次确定一个“当前最近点”
你可以把它想成:
- 你从起点出发扩散
- 但扩散速度不是“按层”(那是 BFS)
- 而是“按当前已知距离最小”
算法维护:
dist[v]:目前已知的最短距离(上界)- 一个优先队列:按
dist最小弹出节点
每次弹出一个节点 u:
- 这意味着在所有未确定的点里,
u是目前最近 - 在非负权条件下,它的距离可以“定格”(不会再被更短路径更新)
这就是正确性的关键。
3. 为什么“定格”是对的?(非负权是关键)
反证直觉:
- 假设
u被弹出时dist[u]不是最短 - 那存在一条更短路径到
u - 这条路径最后一步从某个点
x到u - 因为边权非负,到达
x的距离一定 <= 到达u的距离
但优先队列总会先弹出距离更小的点,矛盾。
所以:
非负权保证了“从更远的点绕回来变得更近”不可能发生。
一旦出现负权边,这个性质就崩了。
4. 实现:邻接表 + 最小堆(优先队列)
JS 没有内置堆,这里给一个最小可用的二叉堆实现(够面试/够用)。
4.1 最小堆
class MinHeap {
constructor() {
this.a = []
}
size() { return this.a.length }
push(item) {
this.a.push(item)
this._up(this.a.length - 1)
}
pop() {
if (this.a.length === 0) return null
const top = this.a[0]
const last = this.a.pop()
if (this.a.length) {
this.a[0] = last
this._down(0)
}
return top
}
_up(i) {
while (i > 0) {
const p = (i - 1) >> 1
if (this.a[p][0] <= this.a[i][0]) break
;[this.a[p], this.a[i]] = [this.a[i], this.a[p]]
i = p
}
}
_down(i) {
const n = this.a.length
while (true) {
let m = i
const l = i * 2 + 1
const r = l + 1
if (l < n && this.a[l][0] < this.a[m][0]) m = l
if (r < n && this.a[r][0] < this.a[m][0]) m = r
if (m === i) break
;[this.a[m], this.a[i]] = [this.a[i], this.a[m]]
i = m
}
}
}
堆里存 [dist, node]。
4.2 Dijkstra
图表示:
const graph = {
A: [['B', 2], ['C', 5]],
B: [['C', 1], ['D', 4]],
C: [['D', 1]],
D: [],
}
算法:
function dijkstra(graph, start) {
const dist = new Map()
const prev = new Map()
const heap = new MinHeap()
dist.set(start, 0)
heap.push([0, start])
while (heap.size()) {
const [d, u] = heap.pop()
if (d !== dist.get(u)) continue // 丢弃过期条目
for (const [v, w] of graph[u] ?? []) {
const nd = d + w
const cur = dist.get(v)
if (cur == null || nd < cur) {
dist.set(v, nd)
prev.set(v, u)
heap.push([nd, v])
}
}
}
return { dist, prev }
}
这里的 d !== dist.get(u) 是关键工程技巧:
- 优先队列里可能有旧距离
- 用它过滤掉“过期弹出”
5. 复杂度
- 使用二叉堆:$O((V+E)\log V)$
- 空间:$O(V+E)$(图 + dist + 堆)
6. 常见坑
- 有负权边还能用吗?
- 不能。用 Bellman-Ford 或 SPFA(谨慎)。
- 重复入堆会不会错?
- 不会,配合“过期条目过滤”即可。
- Dijkstra 能求所有点最短路吗?
- 可以,它是单源到所有点。
7. 面试回答模板
- Dijkstra 解决非负权图的单源最短路。
- 每次从未确定点中选 dist 最小的点定格,并用它松弛邻居。
- 用最小堆实现时间 $O((V+E)\log V)$。
- 有负权边就不适用。
总结
- BFS:无权最短路
- Dijkstra:非负权最短路
- 关键工程点:最小堆 + 过期条目过滤