← 返回博客列表

一、什么是 BFS

广度优先搜索(Breadth-First Search,BFS)是图和树遍历的两大基础策略之一。它的核心思想是:从起点出发,先访问所有距离为 1 的节点,再访问所有距离为 2 的节点,逐层向外扩散,像往水中投石后泛起的涟漪一样一圈圈展开。

一句话定义
BFS = 队列(FIFO) + visited 标记,保证每个节点只在第一次被访问时入队,从而实现"层层扩散"。

它的两大典型应用场景是:

1.1 BFS 与 DFS 的对比

维度 BFS 广度优先 DFS 深度优先
数据结构 队列 queue(FIFO) 栈 stack / 递归(LIFO)
探索方式 层层向外扩散 一条道走到黑再回退
最短路径 天然支持(无权图) 需搜索所有路径后比较
空间复杂度 O(b^d)(b 为分支因子,d 为深度) O(bd)
典型用途 最短路径、层序遍历、最少操作数 连通性、拓扑、所有路径

二、核心原理与图解

BFS 的精髓在于"分层"。同一批入队的所有节点,恰好构成一层。如果在出队时记录层数,就能轻松得到每个节点到起点的距离;如果一次把当前层全部处理完再进入下一层,则称为"分层 BFS"。

2.1 BFS 分层遍历过程

以一个无向图为例,从节点 A 开始 BFS。节点编号 A-G,边连接关系如图。每一层用不同颜色标注:

BFS 分层遍历:从 A 出发,一层层扩散 第 0 层 A 第 1 层 B C D 第 2 层 E F G 第 3 层 H 扩散顺序 A → B C D → E F G → H 同一层的节点会在同一轮队列入队中被处理,距离起点相同
图 1:BFS 分层遍历——同色节点层数相同,扩散方向严格按层推进

2.2 队列状态变化

BFS 的执行过程就是队列的入队/出队交替。下面演示从节点 A 开始,队列在每一步的状态:

BFS 队列状态变化:从 A 出发 步骤 1 起点 A 入队 A 队头→ A ←队尾 步骤 2 A 出队,邻居 B C D 入队 B C D 队头→ B C D ←队尾 步骤 3 B 出队,邻居 E F 入队 C D E F 队头→ C D E F ←队尾 步骤 4 C/D 出队,G 入队 D E F G 队头→ D E F G ←队尾 规律: 每次出队队头,把它的未访问邻居全部加入队尾——队列始终按层数从前往后排列 FIFO 的性质保证了"先进队的先出队",所以层序天然成立
图 2:BFS 队列状态变化——FIFO 保证层数递增

2.3 BFS 与 DFS 探索顺序对比

同样一个图,BFS 和 DFS 走出来的访问顺序差异巨大。下图展示两者在同一个图上的访问轨迹:

BFS vs DFS:同一个图的两种访问顺序 BFS 访问顺序 A B C D E F G A 1 B 2 C 3 D E 4 F 5 G 6 DFS 访问顺序 A B E F G C D A 1 B 2 C 6 D 7 E 3 F 4 G 5 BFS 横向扩散(层数递增),DFS 纵向深入(一条道走到黑)
图 3:BFS 与 DFS 在同一图上的访问顺序对比

2.4 复杂度分析

⏱ 时间复杂度:O(V + E)
每个顶点 V 入队出队各一次,每条边 E 在邻接表存储下被访问一次(无向图每条边访问两次)。

💾 空间复杂度:O(V)
visited 数组占 O(V),队列最坏情况下存所有顶点占 O(V)。

三、通用模板

BFS 的核心结构是队列 + visited 数组。下面给出通用模板,掌握后 80% 的 BFS 题目都能套用。

3.1 基础模板(不分层)

// BFS 通用模板:求从起点 start 到达目标的最短步数
// 适用于"无权图最短路径"或"最少操作数"类问题
int bfs(vector<vector<int>>& grid, int sx, int sy) {
    int m = grid.size(), n = grid[0].size();
    vector<vector<char>> vis(m, vector<char>(n, 0));  // 用 char 节省内存

    queue<pair<int,int>> q;
    q.push({sx, sy});
    vis[sx][sy] = 1;
    int step = 0;

    // 四方向偏移:上下左右
    int dx[] = {-1, 1, 0, 0};
    int dy[] = {0, 0, -1, 1};

    while (!q.empty()) {
        int sz = q.size();           // 当前层的节点数
        for (int i = 0; i < sz; ++i) {  // 处理一整层
            auto [x, y] = q.front(); q.pop();
            if (grid[x][y] == TARGET) return step;  // 找到目标
            for (int k = 0; k < 4; ++k) {
                int nx = x + dx[k], ny = y + dy[k];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                if (vis[nx][ny] || grid[nx][ny] == OBSTACLE) continue;
                vis[nx][ny] = 1;       // 入队时就标记,避免重复入队
                q.push({nx, ny});
            }
        }
        step++;  // 一层处理完,步数加 1
    }
    return -1;  // 找不到
}
模板四要点
1. 分层 BFS:用 int sz = q.size(); for (i = 0; i < sz; i++) 处理当前整层,循环结束后 step++
2. visited 在入队时打标记,不是出队时——否则同一节点会被重复入队,复杂度爆炸
3. 方向数组 dx/dy 是网格类 BFS 的标配,写起来简洁
4. 越界、障碍、已访问三道检查放在邻居扩展时一次性完成

3.2 不分层的连通块模板

// 连通块模板:从每个未访问节点出发,BFS 把整个连通块染上色
void bfsMark(vector<vector<int>>& grid, int sx, int sy) {
    int m = grid.size(), n = grid[0].size();
    queue<pair<int,int>> q;
    q.push({sx, sy});
    grid[sx][sy] = 0;  // 直接修改 grid 充当 visited,省额外空间

    while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        for (int k = 0; k < 4; ++k) {
            int nx = x + dx[k], ny = y + dy[k];
            if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
            if (grid[nx][ny] != 1) continue;
            grid[nx][ny] = 0;
            q.push({nx, ny});
        }
    }
}

四、示例一:岛屿数量

题目(LeetCode 200):给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平/垂直方向上相邻的陆地连接形成。

4.1 示例

输入:

[
  ["11110"],
  ["11010"],
  ["00000"],
  ["00011"]
]

输出:3

岛屿数量:BFS 把每个连通陆地块"淹没" 原始网格 1 1 1 1 0 1 1 0 1 0 1 1 → 三个独立连通块 岛屿 1(6 块) 岛屿 2 岛屿 3(2 块) BFS 从任一陆地格子出发,把整个连通块标 0;外层循环每发现新陆地 count++
图 4:岛屿数量——用 BFS 找出所有连通陆地块

4.2 C++ 代码

#include <vector>
#include <queue>
using namespace std;

int numIslands(vector<vector<char>>& grid) {
    int m = grid.size(), n = grid[0].size();
    int count = 0;
    int dx[] = {-1, 1, 0, 0};
    int dy[] = {0, 0, -1, 1};

    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            // 发现新陆地:开始一次 BFS 把整个岛"淹掉"
            if (grid[i][j] != '1') continue;
            count++;  // 又发现一座岛

            queue<pair<int,int>> q;
            q.push({i, j});
            grid[i][j] = '0';  // 标记已访问(原地修改省空间)

            while (!q.empty()) {
                auto [x, y] = q.front(); q.pop();
                for (int k = 0; k < 4; ++k) {
                    int nx = x + dx[k], ny = y + dy[k];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                    if (grid[nx][ny] != '1') continue;
                    grid[nx][ny] = '0';  // 入队即标记,防止重复入队
                    q.push({nx, ny});
                }
            }
        }
    }
    return count;
}
💡 复杂度
时间 O(m × n),每个格子最多被访问一次;空间 O(m × n),最坏队列里存满整张图(全是陆地)。
关键细节:用 grid[i][j] = '0' 原地标记,省去 visited 数组;入队即标记而非出队时标记,否则同一格子可能被多个邻居重复入队。

五、示例二:最短路径

题目(LeetCode 1091):给定一个 N × N 的二进制矩阵 grid,0 表示通路,1 表示障碍。从左上角 (0,0) 出发到右下角 (N-1, N-1),找出最短路径的长度(每步可以走 8 个方向)。如果不存在返回 -1。

5.1 思路:BFS 天然找最短

因为 BFS 是层层扩散,第一次到达目标的层数一定就是最短步数——这是无权图最短路径的标配方法。注意此题是 8 方向移动,所以方向数组要从 4 个改成 8 个。

8 方向最短路径:BFS 逐层标记距离 0 1 2 2 X 1 2 2 X 3 2 2 3 3 3 X 3 3 4 4 X X 4 4 5 图例 起点(距离 0) 距离 1 距离 2/3/4... 终点(最短 = 5) 障碍 1(不可走) 绿色虚线为最短路径之一
图 5:8 方向最短路径——BFS 给每个格子标距离,终点距离即答案

5.2 C++ 代码

#include <vector>
#include <queue>
using namespace std;

int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
    int n = grid.size();
    if (grid[0][0] == 1 || grid[n-1][n-1] == 1) return -1;  // 起点终点是障碍,直接无解

    // 8 方向偏移:上、下、左、右、四个对角
    int dx[] = {-1,-1,-1, 0, 0, 1, 1, 1};
    int dy[] = {-1, 0, 1,-1, 1,-1, 0, 1};

    queue<pair<int,int>> q;
    q.push({0, 0});
    grid[0][0] = 1;  // 复用 grid 存距离,起点距离为 1(包含自身)

    while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        int dist = grid[x][y];

        // 到达终点:当前距离即答案
        if (x == n-1 && y == n-1) return dist;

        for (int k = 0; k < 8; ++k) {
            int nx = x + dx[k], ny = y + dy[k];
            if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
            if (grid[nx][ny] != 0) continue;  // 非 0 = 已访问或障碍
            grid[nx][ny] = dist + 1;  // 距离 = 当前距离 + 1
            q.push({nx, ny});
        }
    }
    return -1;
}
💡 两个巧妙细节
1. 复用 grid 存距离:把 grid[i][j] 从"0/1 标志位"重用为"距离值",省下单独的 dist 数组
2. 不需要分层 BFS:因为 BFS 的入队顺序天然保证距离递增,普通 while 循环中第一次访问终点即为最短

六、示例三:单词接龙

题目(LeetCode 127):给定两个单词 beginWord 和 endWord,以及一个字典 wordList。从 beginWord 出发,每次只能改变一个字母变成字典中的另一个单词,求从 beginWord 变到 endWord 的最短变换步数。如果不存在返回 0。

6.1 思路:把"单词"看成图节点

关键洞察是把每个单词看作图中的一个节点,两个单词只差一个字母则连一条边。问题就转化为"从 beginWord 到 endWord 的最短路径"——标准 BFS。

单词接龙:构造"差一字母"邻接图后 BFS hit 第 0 层 hot 第 1 层 改 h→h, i→o dot 第 2 层 lot 第 2 层 dog log log dog cog 第 3 层(终点) 绿色虚线为最短路径:hit → hot → lot → dog → cog(4 步变换)
图 6:单词接龙——把单词视为节点,相邻单词连边,BFS 找最短路径

6.2 C++ 代码

#include <string>
#include <vector>
#include <unordered_set>
#include <queue>
using namespace std;

int ladderLength(const string& beginWord,
                  const string& endWord,
                  vector<string>& wordList) {
    unordered_set<string> dict(wordList.begin(), wordList.end());
    if (!dict.count(endWord)) return 0;  // 终点不在字典,无解

    queue<string> q;
    q.push(beginWord);
    int step = 1;  // 起点算第 1 步

    while (!q.empty()) {
        int sz = q.size();  // 当前层单词数
        for (int i = 0; i < sz; ++i) {
            string cur = q.front(); q.pop();
            if (cur == endWord) return step;  // 到达终点

            // 尝试修改每一位的每个字母
            for (int j = 0; j < cur.size(); ++j) {
                string next = cur;
                for (char c = 'a'; c <= 'z'; ++c) {
                    if (c == cur[j]) continue;  // 跳过自身
                    next[j] = c;
                    // 在字典中找到:加入下一层,并从字典删除避免重复访问
                    if (dict.count(next)) {
                        q.push(next);
                        dict.erase(next);  // 关键:入队即删除
                    }
                }
            }
        }
        step++;
    }
    return 0;  // 没找到
}
⚠ 性能关键
dict.erase(next) 在入队时就把单词从字典移除——这是取代 visited 数组的省内存做法。如果改在出队时再删,同一单词会被多个邻居重复入队,时间复杂度退化到 O(N²·L·26)。

七、总结与适用场景

BFS 核心要点

维度 要点
本质 队列 + visited,层层扩散,先到必最短
时间复杂度 O(V + E) 图,O(m × n) 网格
空间复杂度 O(V) 队列 + visited
识别信号 题目问"最短""最少""最近""最少操作数"
关键技巧 分层(用 sz 控制)、入队即标记、方向数组

典型题目清单

🎯 记忆口诀
"队列管序,入队即标;同层同距,先到最短。"
记住这一句,BFS 的精髓就抓住了:队列维持顺序,入队即标防重复,同层就是同距离,第一次到达必为最短。