目录
1384
7 分钟
最短路径:Dijkstra、Bellman-Ford、Floyd 的 TypeScript 实现

概述#

最短路径 研究图中从一个点到另一个点的最低代价路径。根据边权是否为负、是否需要所有点对结果,常用算法包括 Dijkstra、Bellman-Ford 和 Floyd-Warshall。

前置知识

  • 图的邻接表 / 边列表:不同算法适合不同表示
  • 松弛操作:用更短路径更新距离
  • 优先队列:用于优化 Dijkstra 的最小距离节点选择

问题定义#

给定带权图、起点和终点或全部节点,计算路径权重之和最小的路径长度。

要素说明
输入顶点、带权边、起点或所有点
输出单源距离数组、某个目标距离或所有点对距离矩阵
核心操作松弛:dist[v] = min(dist[v], dist[u] + w)
算法选择无负权用 Dijkstra,有负权用 Bellman-Ford,多源用 Floyd

核心原理:分步图解#

Dijkstra 每次确定当前未访问节点中距离最小的点:

graph LR
    A((0)) -- 2 --> B((1))
    A -- 5 --> C((2))
    B -- 1 --> C
    B -- 4 --> D((3))
    C -- 1 --> D

0 出发:

  1. 初始 dist[0] = 0,其他为无穷大。
  2. 选择距离最小的未确定节点。
  3. 用它的出边更新邻居距离。
  4. 重复直到所有可达节点确定。

Dijkstra 依赖“当前最短节点不会再被更新”的贪心性质,因此不能处理负权边。


算法精细步骤#

算法:Dijkstra(graph, start)
输入:非负权图 graph,起点 start
输出:start 到每个节点的最短距离

1. dist 全部初始化为 Infinity,dist[start] = 0
2. 将 [start, 0] 加入优先队列
3. while 队列不为空:
4.     取出距离最小的节点 u
5.     如果该距离已过期,跳过
6.     遍历 u 的所有边 u -> v, 权重 w:
7.         如果 dist[u] + w < dist[v]:
8.             更新 dist[v] 并加入队列
9. 返回 dist

复杂度分析

算法适用场景时间复杂度空间复杂度
Dijkstra 朴素版无负权、稠密图O(V² + E)O(V)
Dijkstra 堆优化无负权、稀疏图O(E log V)O(V + E)
Bellman-Ford可含负权边O(VE)O(V)
Floyd-Warshall所有点对O(V³)O(V²)

TypeScript 实现#

1. 图类型与朴素 Dijkstra#

type WeightedGraph = [number, number][][];

function dijkstra(graph: WeightedGraph, start: number): number[] {
  const n = graph.length;
  const dist = new Array(n).fill(Infinity);
  const visited = new Array(n).fill(false);
  dist[start] = 0;

  for (let i = 0; i < n; i++) {
    let u = -1;
    for (let j = 0; j < n; j++) {
      if (!visited[j] && (u === -1 || dist[j] < dist[u])) u = j;
    }

    if (u === -1 || dist[u] === Infinity) break;
    visited[u] = true;

    for (const [v, weight] of graph[u]) {
      if (dist[u] + weight < dist[v]) {
        dist[v] = dist[u] + weight;
      }
    }
  }

  return dist;
}

2. 堆优化 Dijkstra#

class MinHeap {
  private heap: [number, number][] = [];

  push(item: [number, number]): void {
    this.heap.push(item);
    this.bubbleUp(this.heap.length - 1);
  }

  pop(): [number, number] | undefined {
    if (this.heap.length === 0) return undefined;
    if (this.heap.length === 1) return this.heap.pop();

    const top = this.heap[0];
    this.heap[0] = this.heap.pop()!;
    this.bubbleDown(0);
    return top;
  }

  get size(): number {
    return this.heap.length;
  }

  private bubbleUp(index: number): void {
    while (index > 0) {
      const parent = Math.floor((index - 1) / 2);
      if (this.heap[parent][1] <= this.heap[index][1]) break;
      [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
      index = parent;
    }
  }

  private bubbleDown(index: number): void {
    while (true) {
      const left = index * 2 + 1;
      const right = index * 2 + 2;
      let smallest = index;

      if (left < this.heap.length && this.heap[left][1] < this.heap[smallest][1]) smallest = left;
      if (right < this.heap.length && this.heap[right][1] < this.heap[smallest][1]) smallest = right;
      if (smallest === index) break;

      [this.heap[smallest], this.heap[index]] = [this.heap[index], this.heap[smallest]];
      index = smallest;
    }
  }
}

function dijkstraHeap(graph: WeightedGraph, start: number): number[] {
  const dist = new Array(graph.length).fill(Infinity);
  const heap = new MinHeap();
  dist[start] = 0;
  heap.push([start, 0]);

  while (heap.size > 0) {
    const [node, distance] = heap.pop()!;
    if (distance > dist[node]) continue;

    for (const [next, weight] of graph[node]) {
      const nextDistance = distance + weight;
      if (nextDistance < dist[next]) {
        dist[next] = nextDistance;
        heap.push([next, nextDistance]);
      }
    }
  }

  return dist;
}

3. Bellman-Ford#

function bellmanFord(edges: [number, number, number][], n: number, start: number): number[] {
  const dist = new Array(n).fill(Infinity);
  dist[start] = 0;

  for (let i = 0; i < n - 1; i++) {
    let updated = false;

    for (const [from, to, weight] of edges) {
      if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
        dist[to] = dist[from] + weight;
        updated = true;
      }
    }

    if (!updated) break;
  }

  for (const [from, to, weight] of edges) {
    if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
      throw new Error('存在负权环');
    }
  }

  return dist;
}

4. Floyd-Warshall#

function floydWarshall(graph: number[][]): number[][] {
  const n = graph.length;
  const dist = graph.map(row => [...row]);

  for (let k = 0; k < n; k++) {
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        if (dist[i][k] + dist[k][j] < dist[i][j]) {
          dist[i][j] = dist[i][k] + dist[k][j];
        }
      }
    }
  }

  return dist;
}

工程优化:按图类型选算法#

图特征推荐算法原因
非负权单源Dijkstra + 优先队列速度快,最常用
有负权边Bellman-Ford能检测负权环
所有点对Floyd-Warshall实现简单,适合节点较少
边权都为 1BFS不需要最短路径权重算法
0/1 边权0-1 BFS可用双端队列优化

实际工程中还要注意距离溢出、不可达节点表示、图是否为有向图,以及输入节点编号是否从 0 开始。


应用与局限#

典型应用#

  • 地图导航、路由规划
  • 网络延迟时间、任务链路成本
  • 游戏寻路、图上资源分配
  • 多源距离预处理

局限性#

局限说明
Dijkstra 不支持负权负权会破坏贪心正确性
Floyd 成本高O(V³) 只适合节点数较小的图
图建模影响结果边方向、权重和节点编号必须定义清楚

总结#

graph LR
    A[判断边权] --> B{有负权?}
    B -->|否| C[Dijkstra]
    B -->|是| D[Bellman-Ford]
    A --> E{需要所有点对?}
    E -->|是| F[Floyd-Warshall]

核心要点

  1. 最短路径的核心操作是松弛边。
  2. 无负权单源最短路优先使用 Dijkstra。
  3. 有负权边时使用 Bellman-Ford,并检测负权环。
  4. 所有点对最短路可用 Floyd,但要接受 O(V³) 成本。
最短路径:Dijkstra、Bellman-Ford、Floyd 的 TypeScript 实现
https://www.hehonglei.cn/posts/shortest-path/
作者
Honglei He
发布于
2026-04-26
许可协议
CC BY-NC-SA 4.0