欢迎光临
我们一直在努力

广度优先搜索 (BFS) 完整详解|C# 原生无第三方库实现

广度优先搜索(BFS)基础概念


定义

广度优先搜索(Breadth-First Search,BFS)是一种基于层次化遍历的图搜索算法。该算法的工作原理是:从起始节点出发,首先访问其所有直接相邻节点(第一层),然后依次访问这些相邻节点的相邻节点(第二层),以此类推。这种逐层扩展的遍历方式类似于水波扩散,确保算法总是优先访问距离起始点更近的节点。

核心数据结构

BFS算法的实现必须依赖队列(Queue)数据结构,其先进先出(FIFO)的特性完美契合层级遍历的需求。具体实现步骤如下:

  • 将起始节点加入队列
  • 从队列头部取出当前节点进行处理
  • 将该节点的所有未访问邻居加入队列尾部
  • 重复上述过程直至队列为空

此外,算法需要配合使用访问记录结构(如布尔数组或哈希集合)来标记已访问节点,这对避免图中环路导致的无限循环至关重要。例如,在社交网络分析中,通过标记已访问用户可有效防止重复处理。

算法分类

所属大类:图搜索算法
子类别:

  • 无启发式盲目搜索(不利用目标信息)
  • 通用图/树遍历算法

对比算法:

  • 深度优先搜索(DFS):采用栈结构,优先沿单条路径深入探索
  • A*搜索:结合代价函数与启发式评估的智能搜索方法
  • Dijkstra算法:带权图搜索特例,可视为BFS的加权版本

典型应用场景:

  • 社交网络的好友关系推荐(如三度人脉分析)
  • 网络爬虫的URL抓取策略
  • 迷宫最短路径求解
  • 计算机网络中的广播路由传播

历史背景


起源阶段(1940年代)

1945年,德国计算机先驱Konrad Zuse在研究计算机构造理论时首次提出类似层次遍历的概念。他在设计Z4计算机期间,为解决数据处理中的顺序访问问题,提出了"逐层处理"的算法思想。这一创见为后来的广度优先搜索(BFS)算法奠定了基础,尽管当时尚未形成明确的数学定义。

成型阶段(1950年代)

随着人工智能研究的兴起,BFS算法在解决迷宫导航和机器人路径规划问题时得到了系统发展。1956年,Dijkstra提出的最短路径算法与BFS密切相关。MIT人工智能实验室在开发自动寻路系统时,首次将BFS确立为图遍历的标准方法。这一阶段的特征包括:

  • 主要应用于二维网格地图的路径搜索
  • 确立了队列(Queue)作为核心数据结构
  • 制定了"先访问起始顶点所有邻接点"的基本规则

标准化阶段(1960年代)

随着图论研究的深入,1968年Robert Tarjan等学者在《图论算法导论》等权威著作中,正式将BFS与深度优先搜索(DFS)确立为图遍历的两大基本范式。这一时期的理论突破包括:

  • 严格证明了BFS的最短路径特性
  • 建立了时间复杂度O(V+E)的分析框架
  • 提出了白-灰-黑三色标记法表示访问状态
  • 明确了树边、交叉边等关键概念

现代应用(1990年代至今)

互联网时代为BFS开辟了广阔的应用场景:

  • 社交网络:Facebook采用BFS进行三度人脉挖掘
  • 网络爬虫:Google早期爬虫运用BFS策略进行网页层级抓取,并设置礼貌延迟避免服务器过载
  • 游戏开发:RTS游戏(如星际争霸)利用BFS实现单位寻路和战争迷雾计算
  • 算法面试:二叉树层序遍历成为常见题型,LeetCode收录20余道相关题目
  • 分布式系统:Cassandra等数据库采用BFS变种进行副本同步

核心原理详解


层级遍历规则

BFS(广度优先搜索)采用分层遍历策略:

  • 分层定义
    起始节点为第0层(距离0),其直接邻接节点为第1层(距离1),邻接节点的未访问邻接节点为第2层(距离2),以此类推

  • 遍历顺序
    严格按层级顺序访问节点,必须完整遍历第N层所有节点后才会处理第N+1层节点

  • 应用示例
    社交网络分析中的"一度人脉"(直接好友)、"二度人脉"(好友的好友)等关系层级

队列调度机制

BFS通过队列实现分层遍历:

  • 入队规则
    发现未访问邻接节点时立即加入队列尾部
    代码示例:queue.enqueue(neighbor)

  • 出队规则
    每次从队列头部取出节点处理
    代码示例:current = queue.dequeue()

  • 终止条件
    队列为空表示所有可达节点遍历完成
    非连通图可能需要多次启动BFS

访问标记机制

防止重复访问的关键措施:

  • 数据结构
    常用布尔数组visited[](索引对应节点ID)或哈希表存储状态

  • 标记时机
    节点加入队列时立即标记(非出队时)
    代码示例:visited[node] = true

  • 环路处理
    有效解决双向边(A↔B)或环路场景
    典型应用:防止网页互相引用导致的无限循环

最短路径特性

BFS在无权图中的天然优势:

  • 数学证明
    若目标节点t在第k层首次被访问,则s→t的最短路径长度为k
    更早层级已验证不存在更短路径

  • 路径重建
    通过prev[]数组记录前驱节点
    从终点反向追溯至起点可得完整路径

  • 典型应用
    迷宫最短路径求解
    网络路由的最少跳数计算
    社交关系链查找

复杂度分析

  • 时间复杂度:O(V+E)
    每个顶点和边仅访问一次(V为顶点数,E为边数)

  • 空间复杂度:O(V)
    最坏情况需存储所有节点(如完全图)

广度优先搜索(BFS)执行流程详解(标准无向图)


示例图结构说明

我们以包含5个节点(编号0-4)的无向图为例:

  • 节点0的邻接节点:[1, 2]
  • 节点1的邻接节点:[0, 3]
  • 节点2的邻接节点:[0, 4]
  • 节点3的邻接节点:[1]
  • 节点4的邻接节点:[2]

详细执行步骤

初始化阶段

创建数据结构:

  • 初始化空队列 Queue<int> q 用于存储待访问节点
  • 创建访问标记数组 bool[] visited = new bool[5](共5个节点)
  • 将所有节点的访问状态初始化为 false:[false, false, false, false, false]

处理起点:

  • 将起点节点0标记为已访问:visited[0] = true
  • 将节点0加入队列:q.Enqueue(0)

当前状态:

  • 队列内容:[0]
  • 访问状态:[true, false, false, false, false]
主循环阶段

第一次迭代:

  • 出队:current = 0(队列为空)
  • 处理节点0:
    • 访问邻接节点:[1, 2]
    • 检查节点1:visited[1]为false → 标记为true,入队
    • 检查节点2:visited[2]为false → 标记为true,入队

状态更新:

  • 队列内容:[1, 2]
  • 访问状态:[true, true, true, false, false]
  • 已访问顺序:0

第二次迭代:

  • 出队:current = 1(队列为[2])
  • 处理节点1:
    • 访问邻接节点:[0, 3]
    • 检查节点0:visited[0]为true → 跳过
    • 检查节点3:visited[3]为false → 标记为true,入队

状态更新:

  • 队列内容:[2, 3]
  • 访问状态:[true, true, true, true, false]
  • 已访问顺序:0 → 1

第三次迭代:

  • 出队:current = 2(队列为[3])
  • 处理节点2:
    • 访问邻接节点:[0, 4]
    • 检查节点0:visited[0]为true → 跳过
    • 检查节点4:visited[4]为false → 标记为true,入队

状态更新:

  • 队列内容:[3, 4]
  • 访问状态:[true, true, true, true, true]
  • 已访问顺序:0 → 1 → 2

第四次迭代:

  • 出队:current = 3(队列为[4])
  • 处理节点3:
    • 访问邻接节点:[1]
    • 检查节点1:visited[1]为true → 跳过

状态更新:

  • 队列内容:[4]
  • 访问状态不变
  • 已访问顺序:0 → 1 → 2 → 3

第五次迭代:

  • 出队:current = 4(队列为空)
  • 处理节点4:
    • 访问邻接节点:[2]
    • 检查节点2:visited[2]为true → 跳过

状态更新:

  • 队列为空
  • 访问状态不变
  • 已访问顺序:0 → 1 → 2 → 3 → 4
终止阶段

队列为空,算法结束。所有从节点0可达的节点都已被访问。

完整遍历顺序可视化

开始

0
/ \\
1 2
| |
3 4

遍历顺序:0 → 1 → 2 → 3 → 4

扩展说明

  • 队列的作用:确保节点按"先发现先访问"原则处理,保证广度优先特性
  • 访问标记的意义:防止节点重复访问,特别在无向图中避免往返访问
  • 时间复杂度:
    • 邻接表表示:O(V+E)
    • 邻接矩阵表示:O(V²)
  • 空间复杂度:O(V),主要用于存储队列和访问标记

算法性能分析


时间复杂度 O(V + E)

顶点处理

算法中每个顶点仅被访问一次,包含以下操作:

  • 1次入队操作(enqueue)
  • 1次出队操作(dequeue)
  • 1次访问标记(visited) 总时间复杂度:O(V)
边处理

每条边会被邻接表遍历检查:

  • 有向图:每条边访问1次
  • 无向图:每条边访问2次(双向) 总时间复杂度:O(E)
线性时间复杂度优势

O(V+E)复杂度在数据量增长时表现优异:

  • 相比O(V²)的邻接矩阵实现,当E远小于V²时效率更高
  • 典型应用:社交网络好友关系分析(V=用户数,E=好友关系数)

空间复杂度 O(V)

访问标记数组
  • visited数组大小为V,记录顶点访问状态
  • 空间占用:O(V)
队列存储
  • 最坏情况(如完全图)需存储所有顶点
  • 空间消耗:O(V)
总体空间需求
  • 标记数组和队列大小均与顶点数V成正比
  • 空间复杂度上限始终为O(V),与边数E无关

补充复杂度说明

二叉树层序遍历
  • V = 树节点总数
  • E ≈ V-1(二叉树边数)
  • 复杂度简化为O(V)
  • 典型应用:二叉树层级打印、特定层节点查找
网格迷宫遍历
  • 顶点定义:每个网格单元格为一个顶点
  • 边定义:相邻可移动单元格间的连接
  • 复杂度计算:
    • M×N网格顶点数V=MN
    • 边数E≈4MN(每个单元格最多4条边)
  • 复杂度保持O(MN)
  • 典型应用:最短路径查找、迷宫求解
稀疏图与稠密图
  • 稀疏图(E≈V):复杂度趋近O(V)
  • 稠密图(E≈V²):复杂度趋近O(V²)
  • 工程实践:优先使用邻接表存储稀疏图
与DFS对比
  • 时间复杂度相同:均为O(V+E)
  • 空间复杂度差异:
    • BFS队列存储:O(V)
    • DFS递归栈深:最坏O(V)(线性链状图)
  • 一般情况下BFS空间消耗更大

完整原生代码


无向图 BFS 遍历(邻接表存储)

using System;
using System.Collections.Generic;

namespace BFSAlgorithm
{
/// <summary>
/// 基于邻接表的无向图广度优先搜索实现
/// 纯原生C#,无NuGet、无第三方依赖
/// </summary>
public class UndirectGraphBFS
{
// 邻接表:索引=顶点编号,List存储相邻顶点
private readonly List<int>[] _adjList;
// 顶点总数量
private readonly int _vertexCount;

/// <summary>
/// 构造函数:初始化图
/// </summary>
/// <param name="vertexNum">顶点总数</param>
public UndirectGraphBFS(int vertexNum)
{
_vertexCount = vertexNum;
_adjList = new List<int>[vertexNum];
// 初始化每个顶点的邻接链表
for (int i = 0; i < vertexNum; i++)
{
_adjList[i] = new List<int>();
}
}

/// <summary>
/// 添加无向边:双向关联顶点
/// </summary>
/// <param name="v1">顶点1</param>
/// <param name="v2">顶点2</param>
public void AddEdge(int v1, int v2)
{
_adjList[v1].Add(v2);
_adjList[v2].Add(v1);
}

/// <summary>
/// BFS核心遍历方法
/// </summary>
/// <param name="startVertex">遍历起点</param>
public void BFS(int startVertex)
{
// 1. 初始化访问标记数组
bool[] visited = new bool[_vertexCount];
// 2. 初始化队列存储待遍历顶点
Queue<int> queue = new Queue<int>();

// 起点入队并标记已访问
visited[startVertex] = true;
queue.Enqueue(startVertex);

Console.WriteLine($"BFS遍历序列(起点{startVertex}):");
// 主循环:队列不为空持续遍历
while (queue.Count > 0)
{
// 取出队首当前节点
int current = queue.Dequeue();
Console.Write($"{current} ");

// 遍历当前节点所有邻接点
foreach (int neighbor in _adjList[current])
{
// 未访问则标记并入队
if (!visited[neighbor])
{
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
}
Console.WriteLine("\\n");
}

/// <summary>
/// 程序入口测试
/// </summary>
public static void Main()
{
// 创建5个顶点(0,1,2,3,4)的无向图
UndirectGraphBFS graph = new UndirectGraphBFS(5);
// 添加边构建图结构
graph.AddEdge(0, 1);
graph.AddEdge(0, 2);
graph.AddEdge(1, 3);
graph.AddEdge(2, 4);

// 从顶点0开始BFS遍历
graph.BFS(0);
}
}
}

二维迷宫 BFS 寻路(求解最短路径)

BFS 经典应用案例:基于网格迷宫的最短路径搜索(C# 原生实现),精确计算最小移动步数。

using System;
using System.Collections.Generic;

namespace BFSMaze
{
/// <summary>
/// 迷宫单元格坐标结构体
/// </summary>
public struct Point
{
public int X; // 列
public int Y; // 行
public int Step; // 到达该点的步数

public Point(int x, int y, int step)
{
X = x;
Y = y;
Step = step;
}
}

/// <summary>
/// 迷宫BFS最短路径求解
/// </summary>
public class MazeBFS
{
// 迷宫地图:0=通路,1=墙壁,2=起点,3=终点
private readonly int[,] _maze;
// 迷宫行数、列数
private readonly int _row;
private readonly int _col;
// 上下左右四个移动方向
private readonly int[][] _directions =
{
new int[] {0, -1}, // 上
new int[] {0, 1}, // 下
new int[] {-1, 0}, // 左
new int[] {1, 0} // 右
};

public MazeBFS(int[,] mazeMap)
{
_maze = mazeMap;
_row = mazeMap.GetLength(0);
_col = mazeMap.GetLength(1);
}

/// <summary>
/// 查找起点坐标
/// </summary>
private Point FindStart()
{
for (int y = 0; y < _row; y++)
{
for (int x = 0; x < _col; x++)
{
if (_maze[y, x] == 2)
{
return new Point(x, y, 0);
}
}
}
throw new Exception("迷宫未设置起点(值=2)");
}

/// <summary>
/// BFS求解迷宫最短路径
/// </summary>
public void SolveShortestPath()
{
// 访问标记二维数组
bool[,] visited = new bool[_row, _col];
Queue<Point> queue = new Queue<Point>();
Point start = FindStart();
queue.Enqueue(start);
visited[start.Y, start.X] = true;

bool findTarget = false;
int minStep = 0;

while (queue.Count > 0 && !findTarget)
{
Point current = queue.Dequeue();

// 判断当前点是否为终点
if (_maze[current.Y, current.X] == 3)
{
findTarget = true;
minStep = current.Step;
break;
}

// 遍历四个移动方向
foreach (var dir in _directions)
{
int newX = current.X + dir[0];
int newY = current.Y + dir[1];

// 边界校验、墙壁校验、访问标记校验
if (newX >= 0 && newX < _col
&& newY >= 0 && newY < _row
&& _maze[newY, newX] != 1
&& !visited[newY, newX])
{
visited[newY, newX] = true;
queue.Enqueue(new Point(newX, newY, current.Step + 1));
}
}
}

if (findTarget)
Console.WriteLine($"迷宫最短路径总步数:{minStep}");
else
Console.WriteLine("迷宫不存在可达终点的路径");
}

public static void Main()
{
// 测试迷宫:5行5列
// 2=起点(0,0),3=终点(4,4),1=墙,0=通路
int[,] mazeMap = {
{2,0,1,0,0},
{0,0,1,0,0},
{0,0,0,0,1},
{0,1,1,1,0},
{0,0,0,0,3}
};

MazeBFS maze = new MazeBFS(mazeMap);
maze.SolveShortestPath();
}
}
}

BFS 算法优缺点


优点

天然求解无权图最短路径
  • 原理:采用层级扩散策略,从起点逐层遍历相邻节点,首次到达目标节点时的边数即为最短路径长度
  • 对比优势:相比 DFS 的随机探索路径,BFS 能保证路径最优性
  • 典型应用:社交网络的最短连接链查找(如六度空间理论)、迷宫最短路径求解
遍历结果稳定可预测
  • 确定性:同一图中给定相同起点时,BFS 的访问顺序完全由队列的 FIFO 特性决定
  • 应用价值:便于测试验证,确保工业级图数据库查询结果稳定
  • 实现示例:邻接表存储的图中,遍历顺序由邻接节点存储顺序决定
适配分层业务场景
  • 二叉树层序打印:完美匹配 BFS 的层级遍历特性(如 LeetCode 102 题)
  • 社交圈层推荐:计算用户 1/2/3 度人脉关系
  • 网页抓取策略:限定抓取深度时(如三层内链),优先抓取高权重页面
实现简单高效
  • 核心组件:仅需队列(存储待访问节点)和访问标记结构
  • 代码模板(c#):

using System.Collections.Generic;

void BFS(Node start)
{
Queue<Node> queue = new Queue<Node>();
HashSet<Node> visited = new HashSet<Node>();

queue.Enqueue(start);
visited.Add(start);

while (queue.Count > 0)
{
Node node = queue.Dequeue();
foreach (Node neighbor in GetNeighbors(node))
{
if (!visited.Contains(neighbor))
{
visited.Add(neighbor);
queue.Enqueue(neighbor);
}
}
}
}

  • 面试优势:相比 DFS 的递归实现,BFS 的迭代写法更不易出错

缺点

空间开销较大
  • 内存消耗:最坏情况(完全图)需存储 O(V) 个节点,而 DFS 递归栈最坏为 O(d)
  • 性能对比:遍历 1000 层二叉树时,BFS 队列需存储 2^1000 节点,DFS 仅需 1000 帧栈空间
  • 优化方案:双向 BFS 可减少空间占用,但增加实现复杂度
不适合深度极深的稀疏图
  • 典型场景:链式图(深度为 V-1 但每层仅 1 节点)
  • 数据对比:遍历 100 万节点链式图时,BFS 队列峰值存储 100 万节点
  • 替代方案:迭代加深 DFS(IDDFS)可结合两者优势
无法处理带权图
  • 局限性:假设每步代价相同,无法处理边权差异
  • 反例:当存在总权重更小的长路径时,BFS 会选择边数更少但权重更大的路径
  • 解决方案:Dijkstra 算法(优先队列)或 A* 算法(启发式函数)
无启发式搜索效率低
  • 性能问题:目标节点较远时会遍历大量无关区域
  • 实例分析:在 100×100 网格中搜索对角点,BFS 访问约 7850 个节点(πr²),而 A* 可减少至 200 左右
  • 改进方向:结合启发式函数(如曼哈顿距离)的 Best-First Search

广度优先搜索(BFS)应用场景详解


图论基础应用

层次遍历
  • 适用场景:社交网络中的好友关系可视化
  • 实现原理:从起点出发,依次访问各层级邻接顶点
  • 应用示例:微信好友关系展示(1度好友、2度好友等)
连通性判断
  • 典型场景:交通导航系统查询路线可达性
  • 技术实现:BFS遍历中若到达终点则路径存在
  • 性能优化:可双向BFS同时从起点终点出发
最短路径求解
  • 适用条件:所有边权重相同的图结构
  • 效率优势:时间复杂度O(V+E),优于Dijkstra算法
  • 实际应用:地铁换乘系统计算最少换乘路线
连通分量统计
  • 算法步骤:
    • 从未访问顶点启动BFS
    • 标记所有可达顶点
    • 重复至所有顶点访问完成
  • 应用实例:社交网络中的社群发现

算法面试常见题型

二叉树问题
  • 层序遍历变种:
    • 锯齿形遍历
    • 每层平均值计算
    • 记录层级边界节点
  • 典型题目:LeetCode 102
网格与迷宫
  • 岛屿计数:
    • 扫描二维网格
    • 遇陆地时BFS标记相连区域
    • 统计BFS启动次数
  • 洪水填充:图形处理软件的油漆桶工具实现
单词转换
  • 单词接龙:
    • 构建单词关系图
    • BFS搜索最短转换路径
  • 优化方案:双向BFS提升搜索效率

工程实践应用

社交网络
  • 好友推荐:
    • 一级好友:直接关系
    • 二级好友:共同好友连接
  • 存储优化:结合Redis缓存关系图
网络爬虫
  • 分层抓取:
    • 首页内容
    • 导航栏目
    • 详情页面
  • 反爬策略:设置请求间隔和深度限制
游戏开发
  • 寻路算法:BFS结合启发式函数
  • 碰撞检测:
    • BFS扩散检测范围
    • 实现溅射伤害计算
网络管理
  • 拓扑发现:
    • 从网关BFS扫描
    • 识别相邻设备
    • 构建拓扑图
  • 故障定位:快速诊断网络中断
操作系统
  • 文件系统:
    • 实现tree命令
    • 统计目录大小
  • 进程管理:
    • 分析进程依赖
    • 检测死锁情况

总结


广度优先搜索(BFS)作为计算机科学的基础算法,其基于队列的层级遍历机制,在求解无权图最短路径问题上具有独特优势。本文提供两套可直接运行的C#实现方案,涵盖图遍历和迷宫寻路两大应用场景,系统讲解算法概念、实现原理、执行流程、复杂度分析及适用场景。

BFS与DFS形成算法互补:DFS擅长深度探索和回溯问题,而BFS则专精于分层遍历和最短路径求解。在实际开发中,应根据业务需求合理选择,掌握BFS算法是后端开发、算法面试及游戏开发等领域的核心技能要求

赞(0)
未经允许不得转载:171主机测评 » 广度优先搜索 (BFS) 完整详解|C# 原生无第三方库实现
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址