Javascript算法系列教程:1. BFS求无向无权图最短路径

示例图

3cd9e414-6dfa-4011-a030-5d6c5a685d39

BFS示例代码

function bfsShortestPath(graph, start, end) {
  const queue = [[start]];
  const visited = new Set([start]);
  while (queue.length > 0) {
    const path = queue.shift();
    const node = path[path.length - 1];
    if (node === end) return path;
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push([...path, neighbor]);
      }
    }
  }
  return null;
}

const graph = {
  A: ['C', 'B', 'D', 'E'],
  B: ['A', 'C', 'F', 'D'],
  C: ['A', 'B', 'F'],
  D: ['A', 'B'],
  E: ['A'],
  F: ['B', 'C']
};

console.log(bfsShortestPath(graph, 'C', 'D'));

代码逐行解析

下面逐行解析这段代码在求解 C到D的最短路径 时的执行逻辑,重点跟踪队列、已访问集合的变化和路径探索过程:

1. 函数定义

function bfsShortestPath(graph, start, end) {
  • 定义函数bfsShortestPath,用于计算从start(此处为'C')到end(此处为'D')的最短路径。
  • 参数:graph(邻接表表示的无向无权图)、start(起始节点)、end(目标节点)。

2. 初始化队列和已访问集合

  const queue = [[start]];
  const visited = new Set([start]);
  • queue(队列):存储待探索的路径,初始时只有一条路径[start](即['C'],从C出发的初始路径)。
  • visited(集合):记录已访问的节点,避免重复探索(防止循环),初始时仅包含start(即'C')。

3. BFS核心循环(队列非空时持续探索)

  while (queue.length > 0) {
  • 只要队列中还有待探索的路径,就继续循环(BFS的核心:按“距离递增”顺序探索路径,确保第一个到达目标的路径是最短的)。

4. 取出队首路径并获取当前节点

    const path = queue.shift();
    const node = path[path.length - 1];
  • queue.shift():取出队列的第一个路径(队列“先进先出”特性,保证先探索距离C更近的路径)。
  • node:当前路径的最后一个节点(即“当前正在处理的节点”,后续需探索它的邻居)。

5. 判断是否到达目标节点

    if (node === end) return path;
  • 如果当前节点是目标节点end(即'D'),直接返回当前路径(BFS特性:此时的路径是最短路径)。

6. 遍历当前节点的所有邻居

    for (const neighbor of graph[node]) {
  • 遍历graph中当前节点node的所有邻居(即与node直接相连的节点)。

7. 处理未访问的邻居

      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push([...path, neighbor]);
      }
  • 若邻居未被访问(!visited.has(neighbor)):

    • 标记为已访问(visited.add(neighbor)),避免重复处理。
    • 生成新路径(当前路径 + 该邻居),加入队列等待下一轮探索(queue.push([...path, neighbor]))。

8. 无路径时返回null

  }
  return null;
}
  • 若队列空了仍未找到D,说明C到D无路径,返回null(此处不触发,因为存在路径)。

9. 图的定义(邻接表)

const graph = {
  A: ['C', 'B', 'D', 'E'],
  B: ['A', 'C', 'F', 'D'],
  C: ['A', 'B', 'F'],
  D: ['A', 'B'],
  E: ['A'],
  F: ['B', 'C']
};
  • 定义无向无权图:每个节点的数组值是其直接相邻的节点。例如:

    • C的邻居是A、B、F(C与这三个节点直接相连)。
    • A的邻居包含D(A与D直接相连)。
    • B的邻居包含D(B与D直接相连)。

10. 调用函数并打印结果

console.log(bfsShortestPath(graph, 'C', 'D')); // 输出:['C', 'A', 'D']
  • 调用函数求C到D的最短路径,下面详细分析执行过程

关键执行步骤(C→D的探索过程)

初始状态:

  • 队列:[ ['C'] ](只有从C出发的初始路径)。
  • 已访问:{ 'C' }

第1次循环(处理路径['C']):

  • 取出路径['C'],当前节点node = 'C'

  • 判断:C !== D(继续)。

  • 遍历C的邻居:['A', 'B', 'F'](按数组顺序处理)。

    • 邻居A:未访问 → 标记visited = { 'C', 'A' },新路径['C', 'A']入队 → 队列:[ ['C', 'A'] ]
    • 邻居B:未访问 → 标记visited = { 'C', 'A', 'B' },新路径['C', 'B']入队 → 队列:[ ['C', 'A'], ['C', 'B'] ]
    • 邻居F:未访问 → 标记visited = { 'C', 'A', 'B', 'F' },新路径['C', 'F']入队 → 队列:[ ['C', 'A'], ['C', 'B'], ['C', 'F'] ]

第2次循环(处理路径['C', 'A']):

  • 取出路径['C', 'A'],当前节点node = 'A'

  • 判断:A !== D(继续)。

  • 遍历A的邻居:['C', 'B', 'D', 'E']

    • 邻居C:已访问(跳过)。
    • 邻居B:已访问(跳过)。
    • 邻居D:未访问 → 标记visited = { 'C', 'A', 'B', 'F', 'D' },新路径['C', 'A', 'D']入队 → 队列:[ ['C', 'B'], ['C', 'F'], ['C', 'A', 'D'] ]
    • 邻居E:未访问 → 标记visited = { 'C', 'A', 'B', 'F', 'D', 'E' },新路径['C', 'A', 'E']入队 → 队列:[ ['C', 'B'], ['C', 'F'], ['C', 'A', 'D'], ['C', 'A', 'E'] ]

第3次循环(处理路径['C', 'B']):

  • 取出路径['C', 'B'],当前节点node = 'B'

  • 判断:B !== D(继续)。

  • 遍历B的邻居:['A', 'C', 'F', 'D']

    • 邻居A、C、F:均已访问(跳过)。
    • 邻居D:已访问(第2次循环中已标记)→ 跳过(无新路径入队)。
  • 队列变为:[ ['C', 'F'], ['C', 'A', 'D'], ['C', 'A', 'E'] ]

第4次循环(处理路径['C', 'F']):

  • 取出路径['C', 'F'],当前节点node = 'F'
  • 判断:F !== D(继续)。
  • 遍历F的邻居:['B', 'C'] → 均已访问(无新路径入队)。
  • 队列变为:[ ['C', 'A', 'D'], ['C', 'A', 'E'] ]

第5次循环(处理路径['C', 'A', 'D']):

  • 取出路径['C', 'A', 'D'],当前节点node = 'D'
  • 判断:D === D → 满足条件,返回该路径。

最终结果

函数返回['C', 'A', 'D'],即C到D的最短路径是“C→A→D”(长度为2,经过2条边)。

补充说明

C到D其实还有另一条等长路径“C→B→D”(同样长度为2),但由于BFS中邻居的遍历顺序(C的邻居按['A', 'B', 'F']顺序处理),['C', 'A']路径先入队,因此其延伸出的['C', 'A', 'D']先被探索到,成为函数返回的结果。两条路径都是最短路径,BFS会返回“先被发现”的那条。

posted @ 2025-11-10 17:00  龚思凯1  阅读(6)  评论(0)    收藏  举报