广度优先搜索(BFS)算法详解
一、什么是BFS?
广度优先搜索(Breadth-First Search,简称BFS)是一种用于遍历或搜索树或图的算法。它从根节点开始,先访问所有相邻节点,然后再依次访问这些相邻节点的相邻节点,以此类推,直到找到目标或遍历完整个结构。
二、算法核心思想
BFS的核心特点是按层次探索,就像水波扩散一样,总是先探索距离起点最近的节点。在迷宫问题中,这种特性保证我们找到的路径一定是最短路径。
三、BFS的工作原理
基本流程
-
将起始节点放入队列
-
从队列中取出第一个节点并检查
-
如果找到目标则结束搜索并返回结果
-
否则将它所有未访问的相邻节点加入队列
-
重复步骤2-4直到队列为空
关键数据结构
-
队列(Queue):用于存储待访问的节点,保证先进先出(FIFO)的顺序
-
访问标记(Visited):记录已访问的节点,避免重复访问
四、代码解析
1. 数据结构定义
#define N 110 #define M 10010typedef struct {int x; // 当前点的x坐标int y; // 当前点的y坐标int step; // 从起点到当前点的步数 }P;
N
和M
定义了数组的最大尺寸
P
结构体表示迷宫中的一个点,包含坐标和到达该点的步数
2. 全局变量
int n, m; // 迷宫的行数和列数 int map[N][N]; // 存储迷宫地图,0表示可通行,1表示障碍 int vis[N][N]; // 标记数组,记录哪些点已经访问过P q[M]; // 队列,用于BFS int hh, tt = -1; // 队列的头指针和尾指针int dx[4] = {-1,0,1,0}; // 四个方向的x偏移量 int dy[4] = {0,-1,0,1}; // 四个方向的y偏移量
3. BFS核心函数
void bfs(P p) {q[++tt] = p; // 起点入队while (hh <= tt) { // 队列不为空时循环p = q[hh++]; // 取出队首元素// 如果到达终点,输出步数并返回if (p.x == n-1 && p.y == m-1) {printf("%d", p.step);return;}P next; // 临时存储下一个位置// 检查四个方向for (int i = 0; i < 4; i++) {next.x = p.x + dx[i];next.y = p.y + dy[i];// 如果新位置有效且未访问过if (next.x >= 0 && next.x < n && next.y >= 0 && next.y < m && map[next.x][next.y] == 0 && !vis[next.x][next.y]) {next.step = p.step + 1; // 步数加1q[++tt] = next; // 新位置入队vis[next.x][next.y] = 1; // 标记为已访问}}} }
4. 主函数
int main() {// 读取迷宫尺寸scanf("%d%d", &n, &m);// 读取迷宫数据for (int i = 0; i < n; i++)for (int j = 0; j < m; j++)scanf("%d", &map[i][j]);// 初始化起点P start;start.x = 0;start.y = 0;start.step = 0;// 执行BFSbfs(start);return 0; }
五.图解
队列与BFS的关系
队列(Queue)是一种先进先出(FIFO)的线性数据结构,这与BFS"按层次扩展"的特性完美契合。在BFS中,队列主要有以下功能:
存储待访问节点:保存已发现但尚未探索的节点
控制访问顺序:确保节点按距离起点的层次顺序被处理
维护搜索状态:记录搜索过程中的中间状态
队列操作详解
(1) 初始化队列
使用数组模拟队列,定义头指针
hh
和尾指针tt
初始时
hh=0
,tt=-1
表示空队列(2) 入队操作
q[++tt] = node
:尾指针后移并存入新节点保证新发现的节点排在队列末尾
(3) 出队操作
node = q[hh++]
:取出队首节点并后移头指针确保按发现顺序处理节点
我将用更直观的方式展示广度优先搜索(BFS)如何找到迷宫的最短路径。以下是一个5×5的迷宫示例:
迷宫布局 (0=通路,1=墙壁)
(0,0) (0,1) (0,2) (0,3) (0,4)0 1 0 0 0 (1,0) (1,1) (1,2) (1,3) (1,4)0 1 0 1 0 (2,0) (2,1) (2,2) (2,3) (2,4)0 0 0 0 0 (3,0) (3,1) (3,2) (3,3) (3,4)0 1 1 1 0 (4,0) (4,1) (4,2) (4,3) (4,4)0 0 0 1 0
BFS搜索过程图解
初始状态
起点: (0,0),步数=0
队列: [(0,0,0)]
已访问: (0,0)
[ (0,0,0) ]
第1步:处理(0,0)
可移动方向:
↓ (1,0): 通路,加入队列
→ (0,1): 墙壁,跳过
新队列: [(1,0,1)]
已访问: (0,0), (1,0)
[ (1,0,1) ]
第2步:处理(1,0)
可移动方向:
↓ (2,0): 通路,加入队列
新队列: [(2,0,2)]
已访问: (0,0), (1,0), (2,0)
[ (2,0,2) ]
第3步:处理(2,0)
可移动方向:
↓ (3,0): 通路,加入队列
→ (2,1): 通路,加入队列
新队列: [(3,0,3), (2,1,3)]
已访问: 添加(3,0), (2,1)
[ (3,0,3), (2,1,3) ]
第4步:处理(3,0)
可移动方向:
↓ (4,0): 通路,加入队列
新队列: [(2,1,3), (4,0,4)]
已访问: 添加(4,0)
[ (2,1,3), (4,0,4) ]
第5步:处理(2,1)
可移动方向:
→ (2,2): 通路,加入队列
新队列: [(4,0,4), (2,2,4)]
已访问: 添加(2,2)
[ (4,0,4), (2,2,4) ]
第6步:处理(4,0)
可移动方向:
→ (4,1): 通路,加入队列
新队列: [(2,2,4), (4,1,5)]
已访问: 添加(4,1)
[ (2,2,4), (4,1,5) ]
第7步:处理(2,2)
可移动方向:
↑ (1,2): 通路,加入队列
→ (2,3): 通路,加入队列
新队列: [(4,1,5), (1,2,5), (2,3,5)]
已访问: 添加(1,2), (2,3)
[ (4,1,5), (1,2,5), (2,3,5) ]
继续处理直到找到终点
最终会找到路径到(4,4),此时步数为8。
最短路径可视化
(0,0) → (1,0) → (2,0) → (2,1) → (2,2) → (2,3) → (2,4) → (3,4) → (4,4)步数: 8
关键点说明
-
队列特性:先进先出(FIFO)保证按层次搜索
-
步数记录:每个节点存储从起点到该点的准确步数
-
访问标记:防止重复访问和环路
-
方向顺序:上、左、下、右的顺序检查相邻格子
六.细节问题
p.step是如何更新的?
在BFS中,队列 (q
) 存储待处理的点,而 p
始终是当前正在处理的队列头部节点。步数更新的秘密在于:
-
p
的来源
p
是从队列头部取出的节点(p = q[hh++]
),它的step
值在入队时就已经确定,且之后不会被修改。 -
next.step
的生成
每个新探索的相邻点next
的步数是通过p.step + 1
计算的,表示从p
移动一步到达next
。 -
输出时机
当p
是终点时,直接输出p.step
,因为此时p
的步数已经是起点到终点的最短步数。
七、算法执行流程
-
初始化:将起点(0,0)加入队列,步数为0
-
循环处理队列:
-
取出队首元素
-
如果是终点,返回当前步数
-
否则检查四个方向的相邻点
-
将有效的、未访问的相邻点加入队列,步数+1
-
-
终止条件:队列为空或找到终点
八、关键点分析
-
队列的作用:保证按层次探索,先进先出(FIFO)
-
访问标记数组:防止重复访问,确保每个点只处理一次
-
步数更新:每个新点的步数=当前点步数+1
-
方向处理:通过方向数组实现上、左、下、右的探索
九、实际应用
这种BFS算法可以应用于:
-
迷宫最短路径求解
-
社交网络中的最短关系链查找
-
网页爬虫的层级抓取
-
游戏中的AI路径规划