Skip to content

广度优先搜索(Breadth-First Search)

想象你站在一个巨大的迷宫入口,需要找到出口。有两种策略:

  • 一条路走到黑,撞墙了再回头换路 —— 这是 DFS(深度优先搜索)
  • 像水波纹一样一层一层往外扩散,先检查所有离你 1 步远的地方,再检查 2 步远的 —— 这就是 BFS(广度优先搜索)

BFS 的核心优势在于:在无权图中,它找到的第一条路径一定是最短路径。这是 DFS 做不到的。

原理拆解

BFS 的核心数据结构是队列(Queue)——先进先出。

起点入队 → 出队一个节点 → 把它所有未访问的邻居入队 → 重复直到队列为空

用一张图来演示。假设从节点 A 开始 BFS:

图结构:
    A
   / \
  B   C
 / \   \
D   E   F

BFS 访问顺序:

第 0 层:  队列 [A]           → 访问 A
第 1 层:  队列 [B, C]        → 访问 B、C
第 2 层:  队列 [D, E, F]     → 访问 D、E、F

访问顺序:A → B → C → D → E → F(一层一层往下)

对比 DFS 的顺序是 A → B → D → E → C → F(先往深处钻)。

为什么 BFS 能保证最短路径?

假设从起点 S 到目标 T:

BFS 第 1 层:检查所有距 S 为 1 步的节点
BFS 第 2 层:检查所有距 S 为 2 步的节点
BFS 第 3 层:检查所有距 S 为 3 步的节点
...
BFS 第 k 层:发现 T!→ 最短距离就是 k

因为在第 k 层之前,1 ~ k-1 步能到的节点都已经检查过了,
T 不在其中,所以 k 一定是最短距离。

这就是 BFS 求最短路径的正确性证明——它按距离从近到远逐层扩展,第一个到达目标的路径就是最短的。

BFS 模板

几乎所有 BFS 题都可以套这个模板:

1. 创建队列,起点入队
2. 创建 visited 集合,标记起点
3. while 队列不为空:
   a. 记录当前队列长度 size(当前层的节点数)
   b. for i = 0 to size-1:
      - 出队一个节点
      - 如果是目标 → 返回结果
      - 遍历它的所有邻居
      - 如果邻居未访问 → 标记 + 入队
   c. 层数 +1

代码实现

经典问题一:二叉树层序遍历

LeetCode 102. Binary Tree Level Order Traversal 按层返回二叉树节点的值,每一层一个数组。

TypeScript

typescript
/**
 * 二叉树层序遍历 —— TypeScript 实现
 * 核心:用队列,每层处理 size 个节点
 */
class TreeNode {
  val: number;
  left: TreeNode | null = null;
  right: TreeNode | null = null;
  constructor(val: number) {
    this.val = val;
  }
}

function levelOrder(root: TreeNode | null): number[][] {
  if (!root) return [];

  const result: number[][] = [];
  const queue: TreeNode[] = [root]; // 用数组模拟队列

  while (queue.length > 0) {
    const size = queue.length; // 当前层的节点数
    const level: number[] = [];

    // 一次处理一整层
    for (let i = 0; i < size; i++) {
      const node = queue.shift()!; // 出队
      level.push(node.val);

      // 左右孩子入队(下一层的节点)
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(level); // 一层处理完毕
  }

  return result;
}

// 测试:构建二叉树 [3,9,20,null,null,15,7]
//       3
//      / \
//     9  20
//       / \
//      15  7
const root = new TreeNode(3);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);

console.log(levelOrder(root));
// [[3], [9, 20], [15, 7]]

Go

go
package bfs

// TreeNode 二叉树节点
type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

// LevelOrder 二叉树层序遍历 —— Go 实现
func LevelOrder(root *TreeNode) [][]int {
	if root == nil {
		return nil
	}

	var result [][]int
	queue := []*TreeNode{root}

	for len(queue) > 0 {
		size := len(queue)
		level := make([]int, 0, size)

		for i := 0; i < size; i++ {
			node := queue[0]
			queue = queue[1:] // 出队
			level = append(level, node.Val)

			if node.Left != nil {
				queue = append(queue, node.Left)
			}
			if node.Right != nil {
				queue = append(queue, node.Right)
			}
		}
		result = append(result, level)
	}
	return result
}

Java

java
import java.util.*;

/**
 * 二叉树层序遍历 —— Java 实现
 */
public class LevelOrderTraversal {

    static class TreeNode {
        int val;
        TreeNode left, right;
        TreeNode(int val) { this.val = val; }
    }

    public static List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) return result;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> level = new ArrayList<>();

            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);

                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            result.add(level);
        }
        return result;
    }
}

Python

python
"""二叉树层序遍历 —— Python 实现"""
from collections import deque

class TreeNode:
    def __init__(self, val: int):
        self.val = val
        self.left = None
        self.right = None

def level_order(root: TreeNode | None) -> list[list[int]]:
    if not root:
        return []

    result = []
    queue = deque([root])

    while queue:
        size = len(queue)
        level = []

        for _ in range(size):
            node = queue.popleft()  # O(1) 出队
            level.append(node.val)

            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

        result.append(level)
    return result


# 测试
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
print(level_order(root))  # [[3], [9, 20], [15, 7]]

经典问题二:岛屿数量

LeetCode 200. Number of Islands 给定一个 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿数量。

BFS 策略:遍历每个格子,遇到 '1' 就从它开始 BFS 把整座岛"淹没",岛屿计数 +1。

TypeScript

typescript
/**
 * 岛屿数量 —— TypeScript 实现
 * 遍历每个格子,遇到 '1' 就 BFS 把相连的陆地全标记为 '0'
 */
function numIslands(grid: string[][]): number {
  if (!grid.length) return 0;
  const rows = grid.length,
    cols = grid[0].length;
  let count = 0;

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === "1") {
        count++;
        bfs(grid, r, c, rows, cols); // 把整座岛淹掉
      }
    }
  }
  return count;
}

function bfs(
  grid: string[][],
  startR: number,
  startC: number,
  rows: number,
  cols: number,
) {
  const queue: [number, number][] = [[startR, startC]];
  grid[startR][startC] = "0"; // 标记已访问

  // 四个方向:上、下、左、右
  const dirs = [
    [-1, 0],
    [1, 0],
    [0, -1],
    [0, 1],
  ];

  while (queue.length > 0) {
    const [r, c] = queue.shift()!;

    for (const [dr, dc] of dirs) {
      const nr = r + dr,
        nc = c + dc;
      // 边界内 + 是陆地 → 入队 + 标记
      if (
        nr >= 0 &&
        nr < rows &&
        nc >= 0 &&
        nc < cols &&
        grid[nr][nc] === "1"
      ) {
        grid[nr][nc] = "0";
        queue.push([nr, nc]);
      }
    }
  }
}

// 测试
const grid = [
  ["1", "1", "0", "0", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "1", "0", "0"],
  ["0", "0", "0", "1", "1"],
];
console.log(numIslands(grid)); // 3

Go

go
package bfs

// NumIslands 岛屿数量 —— Go 实现
func NumIslands(grid [][]byte) int {
	if len(grid) == 0 {
		return 0
	}
	rows, cols := len(grid), len(grid[0])
	count := 0

	for r := 0; r < rows; r++ {
		for c := 0; c < cols; c++ {
			if grid[r][c] == '1' {
				count++
				bfsIsland(grid, r, c, rows, cols)
			}
		}
	}
	return count
}

// 四个方向偏移量
var dirs = [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}

func bfsIsland(grid [][]byte, startR, startC, rows, cols int) {
	type point struct{ r, c int }
	queue := []point{{startR, startC}}
	grid[startR][startC] = '0'

	for len(queue) > 0 {
		p := queue[0]
		queue = queue[1:]

		for _, d := range dirs {
			nr, nc := p.r+d[0], p.c+d[1]
			if nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1' {
				grid[nr][nc] = '0'
				queue = append(queue, point{nr, nc})
			}
		}
	}
}

Java

java
import java.util.*;

/**
 * 岛屿数量 —— Java 实现
 */
public class NumberOfIslands {

    private static final int[][] DIRS = {{-1,0},{1,0},{0,-1},{0,1}};

    public static int numIslands(char[][] grid) {
        int rows = grid.length, cols = grid[0].length;
        int count = 0;

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (grid[r][c] == '1') {
                    count++;
                    bfs(grid, r, c, rows, cols);
                }
            }
        }
        return count;
    }

    private static void bfs(char[][] grid, int startR, int startC, int rows, int cols) {
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{startR, startC});
        grid[startR][startC] = '0';

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            for (int[] d : DIRS) {
                int nr = cur[0] + d[0], nc = cur[1] + d[1];
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1') {
                    grid[nr][nc] = '0';
                    queue.offer(new int[]{nr, nc});
                }
            }
        }
    }
}

Python

python
"""岛屿数量 —— Python 实现"""
from collections import deque

def num_islands(grid: list[list[str]]) -> int:
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == "1":
                count += 1
                _bfs(grid, r, c, rows, cols)
    return count

def _bfs(grid, start_r, start_c, rows, cols):
    queue = deque([(start_r, start_c)])
    grid[start_r][start_c] = "0"
    dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]

    while queue:
        r, c = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "1":
                grid[nr][nc] = "0"
                queue.append((nr, nc))

经典问题三:二叉树最小深度

LeetCode 111. Minimum Depth of Binary Tree 从根节点到最近叶子节点的最短路径上的节点数。

BFS 策略:层序遍历,第一个遇到的叶子节点所在的层数就是最小深度。

TypeScript

typescript
/**
 * 二叉树最小深度 —— TypeScript 实现
 * BFS 遇到第一个叶子节点就返回当前层数
 */
function minDepth(root: TreeNode | null): number {
  if (!root) return 0;

  const queue: TreeNode[] = [root];
  let depth = 1;

  while (queue.length > 0) {
    const size = queue.length;

    for (let i = 0; i < size; i++) {
      const node = queue.shift()!;

      // 叶子节点!第一个遇到的就是最近的
      if (!node.left && !node.right) return depth;

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    depth++;
  }
  return depth;
}

Go

go
package bfs

// MinDepth 二叉树最小深度 —— Go 实现
func MinDepth(root *TreeNode) int {
	if root == nil {
		return 0
	}

	queue := []*TreeNode{root}
	depth := 1

	for len(queue) > 0 {
		size := len(queue)
		for i := 0; i < size; i++ {
			node := queue[0]
			queue = queue[1:]

			if node.Left == nil && node.Right == nil {
				return depth
			}
			if node.Left != nil {
				queue = append(queue, node.Left)
			}
			if node.Right != nil {
				queue = append(queue, node.Right)
			}
		}
		depth++
	}
	return depth
}

Java

java
import java.util.*;

/**
 * 二叉树最小深度 —— Java 实现
 */
public class MinDepthTree {

    public static int minDepth(TreeNode root) {
        if (root == null) return 0;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int depth = 1;

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left == null && node.right == null) return depth;
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            depth++;
        }
        return depth;
    }
}

Python

python
"""二叉树最小深度 —— Python 实现"""
from collections import deque

def min_depth(root: TreeNode | None) -> int:
    if not root:
        return 0

    queue = deque([root])
    depth = 1

    while queue:
        for _ in range(len(queue)):
            node = queue.popleft()
            if not node.left and not node.right:
                return depth
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        depth += 1
    return depth

面试题精选

题号题目BFS 思路难度
102二叉树的层序遍历标准 BFS 分层中等
107二叉树的层序遍历 II层序遍历后反转结果中等
111二叉树的最小深度BFS 遇到第一个叶子即返回简单
200岛屿数量多源 BFS,遇到 '1' 就扩散中等
286墙与门多源 BFS,从所有门同时出发中等
127单词接龙BFS 求最短转换序列困难
752打开转盘锁BFS 在状态空间中搜索中等
994腐烂的橘子多源 BFS,每分钟扩散一层中等
199二叉树的右视图层序遍历取每层最后一个中等
429N 叉树的层序遍历把左右孩子换成 children 列表中等

BFS vs DFS:什么时候用哪个?

                    ┌──────────────────────────┐
                    │       问题类型            │
                    └────────────┬─────────────┘
                 最短路径/最少步数 │       存在性/所有路径
                                 ▼               ▼
                            用 BFS           用 DFS
                    ┌────────────┴────────────────┐
                    │                             │
              无权图 BFS               有权图 Dijkstra
              (队列实现)              (优先队列实现)
场景推荐原因
无权图最短路径BFSBFS 天然保证最短
二叉树层序遍历BFS按层处理就是 BFS
连通分量/岛屿问题BFS 或 DFS 都行只是遍历,不涉及最短
路径存在性DFSDFS 代码更短(递归)
所有路径/排列组合DFS回溯更方便
拓扑排序BFS(Kahn)或 DFS两种都行

业务场景

1. 社交网络推荐

"你可能认识的人"功能本质上就是 BFS:从你的好友列表出发(第 1 层),找到好友的好友(第 2 层),推荐那些跟你共同好友最多但你还没加的人。LinkedIn 和 Facebook 的好友推荐系统就是基于 BFS 的变种。

2. 网络爬虫

搜索引擎的爬虫从一个种子 URL 出发,解析页面中的所有链接,把新链接加入队列继续爬取。这就是标准的 BFS——先爬离种子近的重要页面,再慢慢往外扩散。保证高优先级页面优先被抓取。

3. 最短路径导航

地图应用中的步行/驾车导航,底层就是把路网建模成图,用 BFS(或更复杂的 Dijkstra/A*)来找最短路径。地铁站之间的换乘查询就是典型的无权图最短路径问题。

4. 游戏中的 AI 寻路

NPC 要从 A 点走到 B 点,游戏引擎把地图网格化后,用 BFS 找最短路径。更复杂的场景会用 A* 算法(BFS + 启发式函数),但核心思想不变。

5. 版本控制系统的 Diff

Git 在合并分支时,需要找到两个提交的最近公共祖先(LCA)。这个过程就是在提交历史形成的 DAG 上做 BFS。

复杂度分析

操作时间复杂度空间复杂度说明
二叉树 BFSO(n)O(w)w 是树的最大宽度
图的 BFSO(V + E)O(V)V 节点数,E 边数
网格 BFSO(m × n)O(m × n)m 行 n 列
  • 时间复杂度:每个节点最多入队一次、出队一次,每条边最多被遍历一次,所以是 O(V + E)
  • 空间复杂度:最坏情况下队列中存的节点数 = 图的最大宽度。对于满二叉树,最后一层有 n/2 个节点,所以空间 O(n)

小结

BFS 的核心就三句话:用队列、一层一层扩、先到的就是最短的

面试中遇到 BFS 题,记住这几个要点:

  1. 模板要熟:队列 + visited + 分层处理(size = queue.length
  2. 最短路径用 BFS:无权图中 BFS 第一次到达目标就是最短路径
  3. 多源 BFS:多个起点同时入队(如腐烂的橘子、墙与门)
  4. 标记时机:入队时标记 visited,不是出队时(避免重复入队)
  5. 网格题的四个方向:用 dirs = [[-1,0],[1,0],[0,-1],[0,1]] 偏移量,比写四个 if 优雅得多

BFS 口诀:一层一层往外扩,队列先进先出忙。最短路径它最强,到达即是最优解

最近更新