广度优先搜索(BFS)详解及C++实现

广度优先搜索(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的核心实现逻辑固定,均围绕"队列+访问标记"展开,步骤如下:

  1. 初始化队列,将起始节点入队,并标记为已访问;

  2. 当队列不为空时,取出队头节点,访问该节点;

  3. 遍历当前节点的所有邻接节点,若邻接节点未被访问,则标记为已访问并加入队列;

  4. 重复步骤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++实现中,只需围绕"队列初始化-节点入队标记-出队访问-邻接节点入队"的核心流程,就能应对大多数图和树的遍历问题。需要注意访问标记的时机和多连通分量的处理,避免出现重复访问或遗漏节点的问题。

相关推荐
飞天狗1112 小时前
E. Blackslex and Girls
算法
jamesge20102 小时前
限流之漏桶算法
java·开发语言·算法
Funny_AI_LAB2 小时前
Zcode:智谱AI推出的轻量级 AI IDE 编程利器
人工智能·python·算法·编辑器
@卞2 小时前
排序算法(3)--- 交换排序
数据结构·算法·排序算法
youngee112 小时前
hot100-55有效的括号
算法·leetcode·职场和发展
oioihoii2 小时前
C++数据竞争与无锁编程
java·开发语言·c++
嘻嘻嘻开心2 小时前
C语言学习笔记
c语言·数据结构·算法
Blossom.1182 小时前
GPTQ量化实战:从零手写大模型权重量化与反量化引擎
人工智能·python·算法·chatgpt·ai作画·自动化·transformer
睡醒了叭3 小时前
图像分割-传统算法-区域分割
图像处理·人工智能·算法·计算机视觉