广度优先搜索(BFS)详解及C++实现
一、什么是广度优先搜索(BFS)?
广度优先搜索(Breadth-First Search,简称BFS)是一种用于遍历或搜索树或图的经典算法。其核心思想与DFS的"深度优先"截然不同,而是尽可能广地遍历当前节点的所有邻接节点,先遍历完当前层级的所有节点后,再依次遍历下一层级的节点,直到所有节点都被访问完毕。
同样用迷宫比喻理解BFS:走进迷宫后,遇到岔路时不急于深入某一条路,而是先探索当前位置周围所有能直接到达的岔路节点(当前层级),探索完这些节点后,再分别深入每个岔路节点,探索它们周围未被访问的节点(下一层级),以此类推,直到找到出口或遍历完整个迷宫。
BFS的实现必须依赖**队列(Queue)**这种"先进先出(FIFO)"的数据结构。队列的特性恰好能保证"先访问当前层级节点,再访问下一层级节点"的顺序:将当前节点的邻接节点依次入队,待当前节点处理完毕后,再从队列头部取出下一个节点继续处理,循环往复直至队列为空。
二、BFS的核心特性与适用场景
1. 核心特性
-
逐层遍历:优先遍历当前节点的所有邻接节点(同层级),再遍历下一层级节点,呈现"辐射式"扩散效果;
-
队列依赖:通过队列维护待访问节点,保证遍历顺序的"先进先出",无需回溯(与DFS核心差异);
-
空间复杂度:取决于队列的最大容量,最坏情况下为O(n)(n为节点数,如完全二叉树的最后一层节点数接近n/2);
-
时间复杂度:与DFS一致,遍历图时为O(V+E)(V为顶点数,E为边数);遍历树时为O(n)(树中边数为n-1)。
2. 适用场景
-
图的遍历(连通分量查找、最短路径求解,尤其是无权图的最短路径);
-
树的遍历(层序遍历,即按层级从上到下、从左到右访问节点);
-
迷宫问题(求解从起点到终点的最短路径,DFS无法保证最短,BFS是最优选择);
-
拓扑排序(处理有向无环图DAG的节点依赖关系,如任务调度、课程安排);
-
多源最短路径问题(如多个起点到所有节点的最短距离)。
三、BFS的C++实现(队列核心)
BFS的核心实现逻辑固定,均围绕"队列+访问标记"展开,步骤如下:
-
初始化队列,将起始节点入队,并标记为已访问;
-
当队列不为空时,取出队头节点,访问该节点;
-
遍历当前节点的所有邻接节点,若邻接节点未被访问,则标记为已访问并加入队列;
-
重复步骤2-3,直到队列为空,遍历完成。
下面以「无向图的遍历」「二叉树的层序遍历」「无权图最短路径求解」为例,讲解BFS的具体实现。
案例1:无向图的BFS遍历
使用与DFS案例相同的无向图结构:0 → 1 → 2;0 → 3 → 4,采用邻接表存储图,通过队列实现BFS遍历。
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 邻接表存储图
vector<vector<int>> adj;
// 标记节点是否被访问
vector<bool> visited;
// BFS实现函数
void bfs(int start) {
// 初始化队列
queue<int> q;
// 起始节点入队,标记为已访问
q.push(start);
visited[start] = true;
while (!q.empty()) {
// 取出队头节点
int u = q.front();
q.pop();
// 访问当前节点(打印节点值)
cout << u << " ";
// 遍历当前节点的所有邻接节点
for (int v : adj[u]) {
// 未访问的邻接节点标记后入队
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
int main() {
// 图的节点数
int n = 5;
// 初始化邻接表和访问标记数组
adj.resize(n);
visited.resize(n, false);
// 构建无向图(与DFS案例一致)
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[0].push_back(3);
adj[3].push_back(0);
adj[3].push_back(4);
adj[4].push_back(3);
cout << "BFS遍历结果:";
// 从节点0开始遍历(若图不连通,需遍历所有未访问节点)
bfs(0);
cout << endl;
return 0;
}
输出结果:
BFS遍历结果:0 1 3 2 4
说明:BFS按"层级"遍历,先访问起始节点0(第0层),再访问0的所有邻接节点1、3(第1层),最后访问1的邻接节点2、3的邻接节点4(第2层),完美体现"逐层扩散"的特性。
案例2:二叉树的层序遍历(BFS)
二叉树的层序遍历是BFS的典型应用,要求按"从上到下、从左到右"的顺序访问每个节点,与BFS的"逐层遍历"逻辑完全契合。
cpp
#include <iostream>
#include <queue>
using namespace std;
// 二叉树节点定义
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// BFS实现层序遍历
void levelOrderBFS(TreeNode* root) {
// 边界条件:树为空
if (root == nullptr) {
return;
}
// 初始化队列,根节点入队
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
// 取出队头节点
TreeNode* node = q.front();
q.pop();
// 访问当前节点
cout << node->val << " ";
// 左子节点非空则入队(保证左到右的顺序)
if (node->left != nullptr) {
q.push(node->left);
}
// 右子节点非空则入队
if (node->right != nullptr) {
q.push(node->right);
}
}
}
int main() {
// 构建与DFS案例相同的二叉树:
// 1
// \
// 2
// /
// 3
TreeNode* root = new TreeNode(1);
root->right = new TreeNode(2);
root->right->left = new TreeNode(3);
cout << "二叉树层序遍历(BFS):";
levelOrderBFS(root);
cout << endl;
// 释放内存
delete root->right->left;
delete root->right;
delete root;
return 0;
}
输出结果:
二叉树层序遍历(BFS):1 2 3
说明:按层级访问,先访问根节点1(第0层),再访问第1层的2,最后访问第2层的3,符合层序遍历的要求。若需要区分"每一层的节点"(如换行打印每层),可在循环内记录当前队列大小(即当前层的节点数),遍历完该数量的节点后换行,具体优化代码可参考后续补充。
案例3:无权图的最短路径求解
BFS的核心优势之一是能高效求解无权图的最短路径(这里的"最短"指路径上的边数最少)。原理是BFS的逐层遍历特性:第一次访问到目标节点时,经过的层级数就是最短路径长度。我们通过一个简单的无向图,求解从节点0到节点4的最短路径。
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<vector<int>> adj;
vector<bool> visited;
// 记录每个节点到起始节点的最短距离
vector<int> distance;
// BFS求解最短路径(从start到所有节点)
void bfsShortestPath(int start) {
queue<int> q;
q.push(start);
visited[start] = true;
distance[start] = 0; // 起始节点到自身距离为0
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
// 邻接节点的距离 = 当前节点距离 + 1
distance[v] = distance[u] + 1;
q.push(v);
// 若找到目标节点,可直接提前退出(可选)
if (v == 4) {
return;
}
}
}
}
}
int main() {
int n = 5;
adj.resize(n);
visited.resize(n, false);
distance.resize(n, -1); // 初始化距离为-1(未访问)
// 构建无向图(同前序案例)
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[0].push_back(3);
adj[3].push_back(0);
adj[3].push_back(4);
adj[4].push_back(3);
int start = 0, target = 4;
bfsShortestPath(start);
cout << "从节点" << start << "到节点" << target << "的最短路径长度:" << distance[target] << endl;
return 0;
}
输出结果:
从节点0到节点4的最短路径长度:2
说明:节点0到4的路径为0→3→4,共2条边,因此最短路径长度为2。BFS第一次访问到4时,就确定了最短路径,无需后续遍历,效率极高。若使用DFS,需要遍历所有可能路径后才能确定最短,因此无权图最短路径优先选择BFS。
四、BFS的进阶优化:分层遍历(区分每层节点)
在二叉树层序遍历或图的层级遍历中,有时需要明确区分"每一层的节点"(如换行打印每层节点)。核心优化思路是:在每次循环开始时,记录当前队列的大小(即当前层的节点总数),然后只遍历该数量的节点,遍历完后即完成当前层的处理,可执行换行等操作。
cpp
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 分层打印二叉树(BFS进阶)
void levelOrderWithLayer(TreeNode* root) {
if (root == nullptr) {
return;
}
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
// 记录当前层的节点数
int layerSize = q.size();
// 遍历当前层的所有节点
for (int i = 0; i < layerSize; ++i) {
TreeNode* node = q.front();
q.pop();
cout << node->val << " ";
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
// 遍历完当前层,换行
cout << endl;
}
}
int main() {
// 构建一棵更复杂的二叉树:
// 1
// / \
// 2 3
// / \ \
// 4 5 6
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
root->right->right = new TreeNode(6);
cout << "二叉树分层遍历(BFS):" << endl;
levelOrderWithLayer(root);
// 释放内存(简化处理)
delete root->left->left;
delete root->left->right;
delete root->left;
delete root->right->right;
delete root->right;
delete root;
return 0;
}
输出结果:
二叉树分层遍历(BFS):
1
2 3
4 5 6
说明:通过记录每层节点数,成功区分了每一层的节点并换行打印,这种优化在解决"二叉树的层平均值""二叉树的最大宽度"等问题时非常实用。
五、BFS的常见注意事项
-
访问标记时机 :必须在节点入队时标记为已访问,而非出队时。若出队时标记,可能导致同一节点被多次入队(尤其图中有环时),引发重复处理和效率问题;
-
队列的正确使用:严格遵循"入队-标记-出队-访问-邻接节点入队"的流程,确保队列只存储未访问的节点;
-
无向图与有向图的差异:有向图的BFS只需遍历"出边"对应的邻接节点,无需考虑反向边,其他逻辑与无向图一致;
-
多连通分量处理:若图不连通,需遍历所有节点,对每个未访问的节点调用BFS(如:for(int i=0; i<n; i++) if(!visited[i]) bfs(i););
-
最短路径的适用范围 :BFS的最短路径求解仅适用于无权图或边权相等的图。若图为带权图(边权不相等),需使用Dijkstra算法等。
六、BFS与DFS的核心差异对比
为了更清晰地区分两种算法,这里整理了核心差异对比表:
| 对比维度 | BFS | DFS |
|---|---|---|
| 核心思想 | 逐层遍历,辐射式扩散 | 深度优先,回溯探索 |
| 数据结构依赖 | 队列(先进先出) | 栈/递归调用栈(先进后出) |
| 遍历顺序 | 按层级顺序,先广后深 | 按分支顺序,先深后广 |
| 典型应用 | 无权图最短路径、层序遍历、拓扑排序 | 连通分量查找、排列组合、回溯问题(N皇后、数独) |
| 空间复杂度 | 取决于队列最大容量(O(n)) | 取决于递归深度/栈大小(O(n)) |
| 是否保证最短路径 | 是(无权图/边权相等图) | 否 |
七、总结
BFS是一种基于"逐层遍历、队列驱动"的搜索算法,核心优势在于能高效求解无权图的最短路径和实现分层遍历。其代码逻辑固定,易于掌握,关键是理解队列的"先进先出"特性与BFS"逐层扩散"思想的契合度。
在C++实现中,只需围绕"队列初始化-节点入队标记-出队访问-邻接节点入队"的核心流程,就能应对大多数图和树的遍历问题。需要注意访问标记的时机和多连通分量的处理,避免出现重复访问或遗漏节点的问题。