BFS(队列)

BFS算法(队列)

BFS(Breadth-First Search,广度优先搜索)是一种用于遍历或搜索树、图等数据结构的经典算法。其核心思想是:从起始节点出发,优先访问当前节点的所有未访问邻接节点(“广度优先”),待当前层节点全部访问完毕后,再逐层深入访问下一层节点,直至遍历所有可达节点。

  • 特点是“逐层扩散”,适合处理无权图的最短路径、层次关系等问题。
  • 与DFS的“一路深入”不同,BFS通过“横向扩展”保证首次访问节点时,路径长度是最短的(针对无权图)。

一、BFS的应用场景

BFS的“逐层遍历”特性使其在以下场景中具有天然优势:

  1. 无权图最短路径:求两点间的最短路径长度(或具体路径),因首次访问节点即通过最短路径到达。
  2. 层次遍历:树的层序遍历(如二叉树按层输出节点)、图的层级关系分析。
    • 判断最多的潜在转发数:最大到第l层
  3. 连通性判断:与DFS类似,可用于判断图中两点是否连通、统计连通分量个数。
    • 判断连通分量个数:被占领一个城市后的修路问题
  4. 拓扑排序:在有向无环图(DAG)中,结合“入度表”实现拓扑排序(适用于任务调度、依赖关系分析)。
  5. 扩散类问题:如“病毒扩散的最小时间”“迷宫中离起点最近的出口”等。

二、BFS的原理

BFS的遍历过程类似于“水波扩散”:从起点(石子落水点)出发,水波一圈圈向外扩散,先覆盖最近的区域,再逐步扩展到远处。

  • 核心数据结构队列(Queue)。队列的“先进先出(FIFO)”特性完美匹配BFS的“逐层访问”需求——先入队的节点(当前层)优先被处理,其邻接节点(下一层)按顺序后入队。
  • 访问标记:必须通过布尔数组(或哈希表)记录已访问节点,避免重复入队和死循环(尤其图中存在环时)。
  • 实现方式:BFS通常以非递归方式实现(队列手动模拟);递归方式虽可行,但因依赖栈结构,会失去“逐层遍历”的直观性,且易因深度过大导致栈溢出,故不常用。

通用模版

1. 非递归版本(推荐)

非递归是BFS的标准实现方式,通过手动维护队列控制遍历流程:

BFS(起点s) {
  // 1. 初始化:访问标记数组、队列
  初始化visited数组为false;
  queue<int> q;  // 存储待访问节点

  // 2. 起点入队并标记为已访问(入队时标记,避免重复入队)
  q.push(s);
  visited[s] = true;

  // 3. 队列非空时,循环处理节点
  while (!q.empty()) {
    // 3.1 取出队首节点u(当前层节点)
    int u = q.front();
    q.pop();

    // 3.2 处理当前节点u(如输出、记录路径、更新距离等)
    处理u;

    // 3.3 遍历u的所有邻接节点v(下一层节点)
    for (所有从u出发能到达的节点v) {
      if (!visited[v]) {  // 仅处理未访问的节点
        visited[v] = true;  // 标记为已访问
        q.push(v);          // 入队,等待下一层处理
      }
    }
  }
}

队列的常见方法

  1. 判断队列是否为空:q.empty() ,是空则返回true,常见操作为while(!q.empty())用来循环队列
  2. 进入队列: q.puah()
  3. 删除队首元素:q.pop()
  4. 取出队首元素q.front()

2. 递归版本(不常用)

递归实现需额外传递“当前层节点集合”,模拟队列的FIFO特性,实用性较低:

// 辅助函数:递归处理当前层的所有节点
void bfsRecursive(vector<vector<int>>& adj, vector<bool>& visited, queue<int>& q) {
  if (q.empty()) return;  // 队列空,递归终止

  // 取出队首节点u并处理
  int u = q.front();
  q.pop();
  处理u;

  // 邻接节点入队
  for (int v : adj[u]) {
    if (!visited[v]) {
      visited[v] = true;
      q.push(v);
    }
  }

  // 递归处理下一层节点(队列中剩余节点)
  bfsRecursive(adj, visited, q);
}

// 调用入口
BFS(起点s) {
  初始化visited数组为false;
  queue<int> q;
  q.push(s);
  visited[s] = true;

  bfsRecursive(adj, visited, q);  // 启动递归
}

三、BFS的实现步骤(以图为例)

邻接表存储图(适用于稀疏图,效率高于邻接矩阵)为例,详细说明BFS的实现流程:

步骤1:准备数据结构

  • 图的存储:用vector<vector<int>> adj表示邻接表,adj[u]存储节点u的所有邻接节点。
  • 访问标记:用vector<bool> visited标记节点是否已访问,初始化为false
  • 队列:用queue<int>存储待访问节点,控制遍历顺序。
  • (可选)距离记录:若需求是“最短路径”,可增加vector<int> dist数组,dist[v]表示从起点到v的最短距离,初始化为-1(未访问),起点dist[s] = 0

步骤2:核心逻辑(非递归)

  1. 初始化起点:将起点s入队,标记visited[s] = true,若记录距离则dist[s] = 0
  2. 处理队列节点
    • 弹出队首节点u,执行具体处理(如输出、计算距离)。
    • 遍历u的所有邻接节点v
      • v未访问,标记visited[v] = true,入队;
      • 若记录距离,dist[v] = dist[u] + 1(因uv的前一层节点,距离+1即为最短路径)。
  3. 循环终止:队列空时,所有可达节点已遍历完毕。

步骤3:关键特性解释(为何能求无权图最短路径?)

BFS的“逐层遍历”保证:首次访问节点v时,必然是通过从起点到v的最短路径到达的。例如:

  • 起点s为第0层,dist[s] = 0
  • s的邻接节点为第1层,dist = 1(最短路径长度1);
  • 第1层节点的邻接节点(未访问)为第2层,dist = 2(最短路径长度2);
  • 后续层级以此类推,故dist[v]即为起点到v的最短距离。

四、代码示例(C++实现)

以与DFS示例相同的无向图为例(节点0-4),便于对比两种算法的差异:

0 连接 1、2
1 连接 0、3、4
2 连接 0
3 连接 1
4 连接 1

1. 非递归实现(基础遍历+最短路径)

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

// 邻接表存储图
vector<vector<int>> adj;
// 访问标记数组
vector<bool> visited;
// 距离数组:dist[v]表示起点到v的最短距离
vector<int> dist;

// 非递归BFS:从起点start遍历,并计算最短距离
void bfs(int start) {
    int n = adj.size();
    visited.assign(n, false);  // 初始化访问标记
    dist.assign(n, -1);        // 初始化距离为-1(未访问)
    queue<int> q;

    // 起点入队
    q.push(start);
    visited[start] = true;
    dist[start] = 0;  // 起点到自身距离为0

    cout << "BFS遍历结果(非递归):";
    while (!q.empty()) {
        // 取出队首节点并处理
        int u = q.front();
        q.pop();
        cout << u << " ";

        // 遍历邻接节点
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true;
                dist[v] = dist[u] + 1;  // 最短距离+1
                q.push(v);
            }
        }
    }
    cout << endl;
}

int main() {
    // 初始化图(5个节点:0-4)
    int n = 5;
    adj.resize(n);

    // 添加无向边(双向存储)
    adj[0].push_back(1);
    adj[0].push_back(2);
    adj[1].push_back(0);
    adj[1].push_back(3);
    adj[1].push_back(4);
    adj[2].push_back(0);
    adj[3].push_back(1);
    adj[4].push_back(1);

    // 从节点0开始BFS
    bfs(0);

    // 输出起点0到各节点的最短距离
    cout << "起点0到各节点的最短距离:" << endl;
    for (int i = 0; i < n; ++i) {
        cout << "0 → " << i << ":" << dist[i] << endl;
    }

    return 0;
}

输出结果

BFS遍历结果(非递归):0 1 2 3 4 
起点0到各节点的最短距离:
0 → 0:0
0 → 1:1
0 → 2:1
0 → 3:2
0 → 4:2

2. 递归实现(演示用,不推荐)

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

vector<vector<int>> adj;
vector<bool> visited;

// 递归辅助函数
void bfsRecursive(queue<int>& q) {
    if (q.empty()) return;

    // 处理队首节点
    int u = q.front();
    q.pop();
    cout << u << " ";

    // 邻接节点入队
    for (int v : adj[u]) {
        if (!visited[v]) {
            visited[v] = true;
            q.push(v);
        }
    }

    // 递归处理下一层
    bfsRecursive(q);
}

// 递归BFS入口
void bfs(int start) {
    int n = adj.size();
    visited.assign(n, false);
    queue<int> q;

    q.push(start);
    visited[start] = true;

    cout << "BFS遍历结果(递归):";
    bfsRecursive(q);
    cout << endl;
}

int main() {
    // 图初始化与非递归示例相同
    int n = 5;
    adj.resize(n);
    adj[0].push_back(1);
    adj[0].push_back(2);
    adj[1].push_back(0);
    adj[1].push_back(3);
    adj[1].push_back(4);
    adj[2].push_back(0);
    adj[3].push_back(1);
    adj[4].push_back(1);

    bfs(0);  // 输出:0 1 2 3 4
    return 0;
}

五、时间复杂度

BFS的时间复杂度与DFS一致,均取决于图的节点数V和边数E

  • :每个节点仅入队一次(O(V)),每条边仅被处理一次(O(E)),总时间复杂度为 O(V + E)
  • :树是特殊的无环图,边数E = V - 1,时间复杂度简化为 O(V)(如二叉树的层序遍历)。

六、BFS与DFS的核心差异对比

为帮助理解两种算法的适用场景,下表总结关键差异:

维度BFSDFS
核心思想逐层扩散(广度优先)一路深入(深度优先)
数据结构队列(FIFO)栈/递归(LIFO)
路径特性首次访问节点即最短路径(无权图)不保证最短路径,需遍历所有路径对比
空间复杂度取决于最宽层的节点数(可能较高)取决于最深层的节点数(递归易溢出)
典型应用无权图最短路径、层次遍历回溯法(排列/子集)、连通分量

总结

BFS的核心是**“逐层遍历”,通过队列的FIFO特性控制节点访问顺序,其最大优势是能高效求解无权图的最短路径**和层次关系问题。在实现时,需注意“入队时标记访问”以避免重复入队,非递归版本是工程中的首选实现方式。

掌握BFS与DFS的差异,可根据具体问题场景(如“求最短路径”选BFS,“求所有路径”选DFS)灵活选择算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值