【算法图解】BFS:层层推进的搜索

BFS(广度优先搜索)像水波纹一样一层层扩散,天然适合最短路径(无权图)与层级遍历。本文讲清队列实现、visited 的必要性、复杂度、常见题型与工程化写法。

16 分钟阅读
小明

【算法图解】BFS:层层推进的搜索

BFS(Breadth-First Search,广度优先搜索)的感觉像“水波纹”:

  • 从起点出发
  • 先把距离为 1 的点都访问完
  • 再访问距离为 2 的点
  • 直到覆盖全图或找到目标

这使得 BFS 天然适合:

  • 无权图的最短路径
  • 树的层序遍历
  • 最少步数/最少操作次数

1. BFS 的核心:队列

BFS 的数据结构是队列:

  • 先入先出

这正好对应“按层扩散”。


2. 最小可运行模板(图)

图的表示方式很多,这里用邻接表:

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

BFS:

function bfs(graph, start) {
  const queue = [start]
  const visited = new Set([start])
  const order = []

  while (queue.length) {
    const node = queue.shift()
    order.push(node)

    for (const next of graph[node] ?? []) {
      if (!visited.has(next)) {
        visited.add(next)
        queue.push(next)
      }
    }
  }

  return order
}

console.log(bfs(graph, 'A'))

(注意:shift() 在 JS 数组上是 $O(n)$,工程里更推荐用指针或双端队列。)


3. 为什么必须有 visited?

在图里如果没有 visited:

  • 你会在环上无限循环

所以 BFS/DFS 在图中几乎总是需要 visited

在树中通常不需要,因为树没有环。


4. BFS 求最短路径(无权图)

BFS 的关键性质:

  • 第一次到达某个节点时,走过的边数是最少的

实现最短路径,你需要:

  • dist:距离(步数)
  • prev:前驱,用来还原路径
function shortestPath(graph, start, target) {
  const queue = [start]
  const visited = new Set([start])
  const prev = new Map()

  while (queue.length) {
    const node = queue.shift()
    if (node === target) break

    for (const next of graph[node] ?? []) {
      if (!visited.has(next)) {
        visited.add(next)
        prev.set(next, node)
        queue.push(next)
      }
    }
  }

  if (!visited.has(target)) return null

  const path = []
  for (let cur = target; cur != null; cur = prev.get(cur)) {
    path.push(cur)
    if (cur === start) break
  }
  path.reverse()
  return path
}

5. 复杂度

  • 时间:$O(V+E)$(每个点入队一次,每条边最多看一次)
  • 空间:$O(V)$(visited、队列、prev)

6. 典型题型

  • 二叉树层序遍历
  • 最少步数到达终点(迷宫、棋盘)
  • 单词接龙(状态图)
  • 拓扑层级(有向无环图)

做这类题的“套路”是:

  • 把状态看成节点
  • 把一次操作看成边
  • BFS 找最短步数

总结

  • BFS 用队列实现“按层扩散”。
  • 在图里必须 visited。
  • BFS 是无权图最短路径的天然解。