广度优先搜索BFS
中等0广度优先搜索(BFS)——像水波一样层层扩散
什么是广度优先搜索?
想象一下,你往平静的湖面丢一颗小石子,波纹会一圈一圈向外扩散,最先碰到石子的水波一定是离石子最近的那一圈。广度优先搜索(BFS) 正是利用这个原理,像水波一样,从起点开始,先探索所有距离为1的邻居,再探索邻居的邻居(距离为2),一层一层往外推。如果你要找某个目标,最先找到的路径一定是步数最少的(也就是最短路径)。
在生活中,BFS 的应用随处可见:
- 找朋友:你想知道你和某个明星之间通过朋友关系最少需要几个人介绍?从你开始,先问你的所有朋友,如果朋友里没有那个明星,再问朋友的朋友……这就是 BFS。
- 走迷宫:从入口到出口,最少要走多少步?BFS 会帮你找到最短路径。
- 网络游戏:在二维地图中,怪物如何找到前往玩家位置的最短路线?BFS 是其中一种基础算法。
下面我们通过一个具体的图,一步步理解 BFS 的原理和代码实现。
BFS 的核心思想:队列、已访问标记、距离记录
要实现 BFS,我们需要三样工具:
- 队列(Queue):用来存放“待探索的节点”。BFS 先探索起点的邻居,然后探索邻居的邻居,所以必须先进先出(先放入队列的节点先被处理)。Python 的
collections.deque可以实现高效的两端操作。 - 已访问集合(visited):记录哪些节点已经访问过,防止走回头路,避免死循环。
- 距离字典(distance):记录每个节点离起点的最短距离(步数)。每次发现一个新邻居,它的距离 = 当前节点距离 + 1。
举个例子:朋友圈关系图
假设我们有这样一个图,节点是朋友,连线表示两个人认识。
1 --- 2
| |
3 --- 4 --- 5
节点1是你的起点,你想找节点5,最少需要经过几个人介绍?
从1出发,它的邻居是2和3(距离1);然后从2和3出发,它们的邻居包括4(距离2);最后从4出发,邻居包括5(距离3)。所以最短距离是 3 步(1→2→4→5 或 1→3→4→5)。
BFS 的代码实现(分步讲解)
下面用 Python 实现上面的图,用 BFS 找节点5,并打印最短距离。
from collections import deque
# 定义图:用字典表示,每个节点对应一个邻居列表
graph = {
1: [2, 3],
2: [1, 4],
3: [1, 4],
4: [2, 3, 5],
5: [4]
}
def bfs(start, target):
queue = deque([start]) # 队列里放待探索的节点,起点先入队
visited = {start} # 已访问过的节点集合,起点标记为已访问
distance = {start: 0} # 记录每个节点离起点的距离,起点距离为0
while queue: # 只要队列不为空,就继续探索
node = queue.popleft() # 取出队首节点(最先进入队列的)
print(f"当前处理节点:{node},距离起点:{distance[node]}")
if node == target: # 如果当前节点就是目标,输出距离并结束
print(f"找到目标 {target}!最短距离为 {distance[node]}")
return distance[node]
# 遍历当前节点的所有邻居
for neighbor in graph[node]:
if neighbor not in visited: # 只处理未访问过的邻居
visited.add(neighbor) # 标记为已访问
distance[neighbor] = distance[node] + 1 # 距离加1
queue.append(neighbor) # 将邻居放入队尾,等待后续探索
print(f"没有找到目标 {target}")
return None
# 调用函数
bfs(1, 5)
运行过程可视化(理解队列如何工作):
- 初始:队列 = [1],visited = {1},distance = {1:0}
- 弹出1,检查:不是目标,遍历邻居2和3,均未访问 → 入队2和3,distance[2]=1,distance[3]=1,visited增加{2,3},队列变为 [2,3]
- 弹出2,邻居1已访问,邻居4未访问 → 入队4,distance[4]=2,队列变为 [3,4]
- 弹出3,邻居4已访问(因为从2已经加入),邻居1已访问 → 无新节点,队列变为 [4]
- 弹出4,邻居2、3已访问,邻居5未访问 → 入队5,distance[5]=3,队列变为 [5]
- 弹出5,发现是目标,输出距离3。
关键点解释
- 队列的先进先出保证了“层层推进”:距离为1的节点2和3先入队,它们会先于距离为2的节点4被处理。这样,当我们第一次找到目标时,它一定是所有可能路径中步数最少的。
- visited 为什么重要:如果没有 visited,从节点2访问邻居4后,从节点3访问邻居4时又会把4加入队列,导致重复处理,甚至死循环(如果图中有环)。
- distance 的更新时机:只在第一次发现邻居时更新,这样每个节点的距离就是最短距离。因为 BFS 按层探索,第一次访问到某个节点时,路径一定是最短的。
新手常犯的错误
-
忘记标记 visited:导致节点被重复加入队列,不仅效率低,还可能造成队列爆炸或无限循环(尤其在有环的图中)。
# 错误示例 queue.append(neighbor) # 应该先标记 visited,再入队 -
队列操作错误:用了列表的 pop(0) 实现队列?Python 列表的 pop(0) 是 O(n) 操作,效率极低,应该用
collections.deque的popleft()。# 错误 queue = [start] node = queue.pop(0) # 慢! -
距离更新错误:有时新手会用
distance[node] = distance.get(node, 0) + 1,但这样可能重复更新已存在的节点距离,导致距离被错误累加。正确做法是 只在第一次发现邻居时更新。 -
忽视图的表示方式:BFS 适用于任何可以用图表示的问题。如果是网格(迷宫),需要自己处理上下左右四个方向的邻居,别忘了边界检查。
-
没有考虑起点就是目标的情况:如果 start 等于 target,应该直接返回 0。上面的代码中,如果 start==target,在第一轮循环就会输出距离0。
完整示例:用 BFS 走迷宫(二维网格)
除了普通图,BFS 最经典的应用是走迷宫。我们来看一个具体例子:在 4x4 的网格中,从起点 (0,0) 走到终点 (3,3),0 表示可走,1 表示障碍。
from collections import deque
# 迷宫:0可走,1障碍
maze = [
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[1, 0, 0, 0]
]
def bfs_maze(start, end):
rows, cols = len(maze), len(maze[0])
# 四个方向:上、下、左、右
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# 队列存放 (行, 列) 坐标
queue = deque([start])
visited = {start} # 已访问的坐标集合
distance = {start: 0} # 记录每个坐标离起点的距离
while queue:
row, col = queue.popleft()
if (row, col) == end:
print(f"到达终点,最短步数:{distance[(row, col)]}")
return distance[(row, col)]
# 尝试四个方向
for dr, dc in directions:
nr, nc = row + dr, col + dc
# 检查是否在迷宫范围内,是否可走,是否未访问
if 0 <= nr < rows and 0 <= nc < cols and maze[nr][nc] == 0 and (nr, nc) not in visited:
visited.add((nr, nc))
distance[(nr, nc)] = distance[(row, col)] + 1
queue.append((nr, nc))
print("无法到达终点")
return None
# 调用
bfs_maze((0, 0), (3, 3))
运行结果:从 (0,0) 到 (3,3) 的最短步数为 6(可手算验证)。
总结与相关指引
- BFS 的特点:适用于 无权图(每条边权重相同)中求最短路径,也适用于 状态空间搜索(如八数码、单词接龙)。
- 时间复杂度:O(V + E),其中 V 是节点数,E 是边数(每个节点和每条边都访问一次)。
- 空间复杂度:O(V),需要存储队列和 visited。
接下来可以学习:
- 深度优先搜索(DFS):使用栈(或递归),适合探索所有路径、拓扑排序等。
- Dijkstra 算法:解决带权图的最短路径问题(权重非负)。
- A 搜索*:在 BFS 基础上加入启发式函数,更快找到目标。
现在,你可以试着修改上面的迷宫代码,比如增加障碍物、改变起点终点,或者把图改成你身边的朋友关系,看看用 BFS 能不能帮你找到你和某个偶像之间最少需要几个人介绍!
例题精讲
在广度优先搜索(BFS)算法中,通常使用哪种数据结构来存储待访问的节点?
在无权图中,广度优先搜索(BFS)一定能找到从起点到终点的最短路径。
以下是用BFS遍历图的Python代码片段,请补充缺失部分(使用collections.deque)。
from collections import deque
def bfs(graph, start):
visited = set()
queue = ___
visited.add(start)
while queue:
node = queue.popleft()
print(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
___
给定一个二维网格(0表示空地,1表示障碍物),从左上角(0,0)出发,每次只能向上、下、左、右移动一格,到达右下角(n-1,m-1)的最短路径长度是多少?若无法到达,返回-1。以下哪种算法最适合解决该问题?
在广度优先搜索(BFS)中,所有节点的访问顺序是按照它们的深度(距离起点的边数)递增的。