Skip to content

广度优先搜索(BFS)

2026 大纲 五(三)图的遍历 2. 广度优先搜索(第 1 小项见《DFS》,DFS 与 BFS 的完整对照表也在那一篇)。

一圈一圈往外推

DFS 是"一条路走到黑",BFS 正好相反:先访问起点,再访问它的全部邻接点,然后访问这些邻接点的全部未访问邻接点。像往水里丢一颗石子,波纹一圈一圈往外扩。

要做到这一点,得记住"已经访问过、但邻居还没展开"的那些顶点,并且按记录的先后顺序去展开。"先记的先取"正是队列(FIFO)的语义,所以 BFS 用队列。

这里有个值得停一下的观察:

🔴 把队列换成栈,同一份代码就变成了 DFS。 遍历策略并不写在算法骨架里,它藏在辅助容器"取出元素的顺序"里。

先动手看一眼

加载可视化中...

盯住队列那一栏:每出队一个顶点,它的未访问邻居就整批涌进队尾。同一层的顶点总是挨在一起进队——这个现象就是后面"层次划分唯一"的全部内容。

入队即置位,这一条不能挪

c
// ── 邻接表版
void BFS_AdjList(int v) {
    int queue[MAX_VERTEX], front = 0, rear = 0;
    printf("%d ", v);  visited[v] = 1;  queue[rear++] = v;
    while (front != rear) {
        int u = queue[front++];
        ArcNode *p = AdjList[u].first;
        while (p != NULL) {
            int w = p->adjvex;
            if (!visited[w]) {
                printf("%d ", w);
                visited[w] = 1;          // 置位与入队同时发生
                queue[rear++] = w;
            }
            p = p->next;                 // 🔴 推进必须在 if 之外,否则死循环
        }
    }
}

// ── 遍历整个图(处理非连通图);启动次数 = 无向图连通分量个数
void BFSTraverse(void) {
    for (int i = 0; i < n; i++) visited[i] = 0;
    for (int i = 0; i < n; i++)
        if (!visited[i]) BFS(i);
}

visited[w] = 1 必须与入队同时发生,挪到出队时才置位是错的。看个反例:图 01, 02, 12。0 出队后 1、2 都入队(此刻都还没置位);接着 1 出队、置位,检查它的邻居 2 时,2 仍然是未置位的——于是 2 被第二次入队。稠密图上这种重复会被放大得非常厉害。

这条纪律还顺带保证了另一件事:每个顶点最多入队一次rear 最大只到 n,所以用普通顺序队列就够了,不必上循环队列。反过来说,一旦把置位挪到出队,队列就可能越界。

外层的 for 循环同样不能省:一次 BFS 只走遍一个连通分量。启动次数 = 无向图连通分量个数(有向图上与强连通分量个数没有对应关系,与 DFS 同理)。

复杂度与 DFS 完全相同:邻接表 O(n+e)(顶点入队出队各一次给出 n,每个边结点被检查恰好一次给出 e),邻接矩阵 O(n2)(每个顶点出队一次,每次都要扫完一整行)。两者对顶点和边的访问次数一模一样,只是次序不同。

空间也是 O(n),但最坏形态与 DFS 相反:BFS 的空间瓶颈是队列,最坏是星形图——从中心出发第一步就把 n1 个顶点全压进队列;DFS 的瓶颈是递归栈,最坏是链形图。所以"很深但分支少"的图 BFS 省空间,"很宽但很浅"的图 DFS 省空间。

层次划分唯一,序列不唯一

这是 BFS 最要紧的一条性质,也是它能求最短路径的全部根据。

层号 d(v)(起点到 v 的最短边数)是图的固有量,与实现方式无关:不管邻接表怎么建、同层内谁先谁后,v 落在第几层都是定的。

序列则会变:起点不同、每个顶点的邻接点检查次序不同,序列就不同。但同层内部怎么排,都不影响任何一个顶点的层号

🔴 层次划分唯一 + 序列不唯一 = BFS 求最短路径是可靠的,而具体的遍历序列不是唯一答案。判"下列哪个不是 BFS 序列"的题就建立在后半句上。

由此还得到 BFS 树的一条 DFS 树没有的性质:

🔴 跨层不超过 1:无向图中对任意边 (u,v)E,有 |d(u)d(v)|1

理由:设 d(u)=ku 出队展开时 v 要么早已被访问(层号 k),要么此刻才被访问(层号 =k+1),所以 d(v)d(u)+1;对称地 d(u)d(v)+1。这就是"BFS 树扁宽、DFS 树细长"的形式化说法。

BFS 生成树由所有"引起顶点首次被访问"的边构成。树上从根到任一顶点的路径,就是原图中边数最少的路径——因为 w 首次被访问时是从某个第 k 层顶点展开的,所以它落在第 k+1 层;若真有更短的路径,它早就该更早出现了。实现细节见 BFS 最短路径

判一个序列是不是合法的 BFS 序

给一张图和四个序列,问哪个不是 BFS 序列。判法是逐个卡两条:

  1. 序列的第一个顶点是起点,它自己一层。
  2. 从第 2 个开始,每个顶点必须是"当前已出队顶点"的未访问邻居。换句话说,把序列切成层:第 1 层是起点的全部邻居,第 2 层是这些邻居的全部未访问邻居……每一层必须整块出现,不能有某层的顶点插到下一层中间去

最快的手法是先按起点算出每个顶点的层号,再看序列的层号是不是单调不减。层号跳回去了,或者某一层还没排完就出现了下一层的顶点,那就不合法。

⚠️ 同层内部的次序是自由的,别拿"编号从小到大"去否定选项——那样会把好几个合法序列判成非法。

BFS 的"最短"是边数最少,不是权和最小

这句话要拆成两个方向记,因为真题两个方向都考过。

方向一:带权图上 BFS 不能求最短路。 BFS 把每条边都当作等长的一步,只保证边数最少。反例:01 权 100、02 权 1、21 权 1。BFS 认为 01 最短(只用 1 条边),而真正的最小权路径是 021(权和 2)。带权图要用 Dijkstra

方向二:各边权都相等的图上,BFS 恰好就是最短路算法。 权全为 1 时"边数最少"与"权和最小"是同一件事,BFS 直接给出答案。

第二个方向有一处特别值得留意——它常常和最小生成树摆在一起考:

🔴 PrimKruskal 求出的最小生成树,不是最短路径树。 即使各边权都为 1、MST 的总权值确实最小,树上从某个顶点到其余顶点的路径也未必最短。

举个最小的例子:四个顶点连成一个环 abcda,各边权都是 1。MST 要去掉一条边,比如去掉 (d,a),得到链 abcd。此时树上 ad 要走 3 条边,而原图里 ad 直接相邻、距离是 1。MST 优化的是"所有边的权值总和",最短路径树优化的是"从源点到每个顶点各自的距离",两个目标不一样。

与树的层序遍历的关系

二叉树的层序遍历就是 BFS 在树上的特例,代码几乎一样,差别只有一处:树里不需要 visited[]。因为树中从根到任一结点的路径唯一,一个结点不可能从两条路径被到达;图里有环,所以必须有它。

七顶点无向图的 BFS 走查与层次划分,以及与 DFS 序列的对照(想手动模拟就展开)

DFS 那篇用的是同一张无向图,便于对照:E={(0,1),(0,2),(1,3),(1,4),(2,5),(2,6),(4,5)},邻接表按编号升序:

顶点边链表
01 → 2
10 → 3 → 4
20 → 5 → 6
31
41 → 5
52 → 4
62

从顶点 0 出发:

出队顶点检查其邻居新入队队列状态(出队后)已输出
000
101, 21, 20 1 2
210(已), 3, 43, 40 1 2 3 4
320(已), 5, 65, 60 1 2 3 4 5 6
431(已)0 1 2 3 4 5 6
541(已), 5(已)0 1 2 3 4 5 6
652(已), 4(已)0 1 2 3 4 5 6
762(已)0 1 2 3 4 5 6

BFS 序列:0 1 2 3 4 5 6,层次划分为 L0={0}L1={1,2}L2={3,4,5,6}

与 DFS 的对照:同一张图、同一个起点、同一份邻接表,DFS 序列是 0 1 3 4 5 2 6。DFS 在第 2 步就一头扎到 3 号顶点(离起点 2 条边),BFS 则把离起点 1 条边的顶点全部处理完才往下走。

序列的不唯一性:若把顶点 0 的链改成 2 → 1、顶点 1 的链改成 4 → 3 → 0、顶点 2 的链改成 6 → 5 → 0,得到 0 2 1 6 5 4 3——层次划分不变,只是同层内部的次序变了。

BFS 生成树:树边为 (0,1),(0,2),(1,3),(1,4),(2,5),(2,6),共 6=n1 条;唯一的非树边是 (4,5)d(4)=d(5)=2,层号之差为 0,符合"跨层不超过 1"。

无向图的广度优先搜索过程与它的 BFS 树

图注:左图顶点旁的数字是访问次序,虚线把顶点按"距起点的层"切开—— 一层处理完才推进到下一层,虚线就是波纹的形状。右图是把"首次到达"用的那些边抽出来构成的 BFS 树。与 DFS 那篇同一张原图的 DFS 树对比:这里 A 直接连着 B,C,D 三个孩子, 树很矮很宽;DFS 树则是一条长链。同一个图,两种遍历给出形状完全不同的生成树。 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.13 广度优先搜索示例,p366

由"跨层不超过 1"直接得到的二部图判定法(要写这段代码时展开)

跑一遍 BFS 求出每个顶点的层号 d(v),再检查每条边:

  • 每条边两端层号奇偶性都不同(差恰为 1),按层号奇偶把顶点分成两组就是一个合法二分,图二部图;
  • 若存在同层边 (u,v)d(u)=d(v)=k),则"根→u"(k 条边)+ 边 (u,v) + "v→根"(k 条边)构成一条长度 2k+1奇数长度闭合回路,图中必含奇环,不是二部图(判定定理见图的基本概念)。

落成代码就是"相邻必异色":

c
int color[MAX_VERTEX];      // -1 未染色;0 / 1 两种颜色

int isBipartite(int n) {
    for (int i = 0; i < n; i++) color[i] = -1;
    int queue[MAX_VERTEX];
    for (int s = 0; s < n; s++) {
        if (color[s] != -1) continue;       // 该分量已处理过
        int front = 0, rear = 0;
        color[s] = 0;
        queue[rear++] = s;
        while (front != rear) {
            int u = queue[front++];
            for (ArcNode *p = AdjList[u].first; p; p = p->next) {
                int w = p->adjvex;
                if (color[w] == -1) {
                    color[w] = 1 - color[u];   // 相邻必异色 → 层号奇偶交替
                    queue[rear++] = w;
                } else if (color[w] == color[u])
                    return 0;                  // 同色边 → 存在奇环 → 不是二部图
            }
        }
    }
    return 1;
}

两处要点:

  1. 外层的 for (s) 循环不能省。判定要对每个连通分量各做一次,否则非连通图只判了一部分。
  2. else if 里的 else 不能丢w 已染色时才需要检查冲突;刚染上的色必然是异色,重复检查是多余的。

用走查那张图跑:color[0]=01,2 染 1;3,4,5,6 染 0。检查边 (4,5) 两端都是 0,同色 → 不是二部图 ✓,与奇环 014520(长度 5)一致。

邻接矩阵版实现,以及用 BFS 数连通分量
c
// ── 邻接矩阵版:每次出队都要扫一整行才能找齐 u 的邻接点
void BFS(int v) {
    int queue[MAX_VERTEX], front = 0, rear = 0;
    printf("%d ", v);
    visited[v] = 1;
    queue[rear++] = v;                   // 入队与置位同时完成
    while (front != rear) {
        int u = queue[front++];
        for (int w = 0; w < n; w++)
            if (Graph[u][w] == 1 && !visited[w]) {
                printf("%d ", w);
                visited[w] = 1;
                queue[rear++] = w;
            }
    }
}

int countComponents(void) {
    for (int i = 0; i < n; i++) visited[i] = 0;
    int cnt = 0;
    for (int i = 0; i < n; i++)
        if (!visited[i]) { BFS(i); cnt++; }   // 每启动一次 BFS 就多一个分量
    return cnt;
}

走查:7 个顶点,边为 (0,1),(1,2),(3,4),(5,6)i=0 未访问 → BFS(0) 访问 {0,1,2}cnt=1i=1,2 已访问跳过;i=3BFS(3) 访问 {3,4}cnt=2i=5BFS(5) 访问 {5,6}cnt=3连通分量数 = 3。

同时得到 BFS 生成森林:三棵树的边分别是 {(0,1),(1,2)}{(3,4)}{(5,6)},共 4 条 =nk=73 ✓。

⚠️ 这条只对无向图成立

考点速记

三条结论:

  1. 队列 → 逐层扩展;把队列换成栈就变成 DFS,算法骨架不变。
  2. visited 必须在入队时置位,这同时保证了正确性与"每个顶点最多入队一次"。
  3. 层次划分唯一、序列不唯一;BFS 树上任意一条原图边的两端层号之差不超过 1。

这一节在真题里被考过的形式(下方「真题练习」逐题对应):

  • 判某序列不是 BFS 序列:四个选项里三个合法。手法是先按该起点算出各顶点的层号,再看序列的层号是否单调不减、每层是否整块出现。同层内部次序自由,别按编号大小否定。
  • 邻接表上 BFS 的时间复杂度O(n+e),四个选项通常是 O(n) / O(e) / O(n+e) / O(n×e)
  • 等权图上谁能求最短路:给一个"各边权均为 1 的无向连通图",问 Prim / Kruskal / BFS 中哪些一定能求出从某顶点到其余各顶点的最短路径。答案只有 BFS——最小生成树最小化的是全树权值和,不是各顶点到源点的距离。
  • 带权图上 BFS 行不行:作为错误命题出现,"可用 BFS 求带权图中每一对顶点的最短路径"是错的。

易错BFS 的"最短"是边数最少。 边权不全相等时它给出的不是最短路,要用 Dijkstra;边权全相等时它才恰好等价于最短路算法。

易错最小生成树不是最短路径树。 四顶点等权环去掉一条边得到的链,就是"MST 权和最小、但树上两点距离比原图远"的最小反例。

易错visited 在入队时置位,不是出队时。 挪到出队,同一个顶点会被重复入队,队列还可能越界。

教材出处
  • 严蔚敏《数据结构(C 语言版)》(第 2 版)印刷 p164,§6.5.2: BFS 的三步过程描述,以及"先访问的顶点其邻接点亦先被访问。为此,算法实现时需引进队列 保存已被访问过的顶点";同页给出 BFS 算法(算法 6.7), 在 if (!visited[w]) 分支内同时完成访问、置 visited[w]=true 与入队。
  • 同书印刷 p164:广度优先生成树—— "图 6.17(c) 中所示的所有顶点加上标有实箭头的边,构成一棵以 v1 为根的树, 称为广度优先生成树"。
  • 同书印刷 p164(算法分析): "每个顶点至多进一次队列……广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同, 即当用邻接矩阵存储时,时间复杂度为 O(n2);用邻接表存储时,时间复杂度为 O(n+e)。 两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。"
  • 同书印刷 p170(§6.6.2 开头): 求"中转次数最少"的路线时"只需从顶点 A 出发对图做广度优先搜索…… 由此所得的广度优先生成树上,从根顶点 A 到顶点 B 的路径就是中转次数最少的路径"。

图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.13,p366。

相关知识

DFS(栈驱动的对称版本,完整对照表在那一篇)| BFS 最短路径d[]path[] 的实现与正确性证明)| 二叉树的层序遍历(BFS 在树上的特例)| 队列(BFS 的辅助结构)| 拓扑排序(Kahn 用的是同一套队列框架)| Dijkstra 算法(带权图上的"BFS",队列换成按 dist 取最小的优先队列)

真题练习