其实广度优先搜索这东西,说复杂也复杂,说简单也简单。你可以把它想象成小时候玩迷宫,从入口开始,先把周围能一步走到的地方都看一遍,记下来;然后再从这些地方出发,看下一步能到哪些新地方,就这么一层一层往外扩,直到找到出口。这种一层一层 "扫荡" 的思路,就是广度优先搜索最核心的东西。
在代码里,这种思路通常用队列来实现。因为队列的特性是 "先进先出",正好能保证我们先处理完当前层的所有节点,再去处理下一层的。就像排队打饭,先来的先打,打完了后面的再上,不会乱了顺序。
拿力扣上的 102 题 "二叉树的层序遍历" 来说,这题简直是为展示 BFS 量身定做的。题目要求我们按照层级顺序返回二叉树的节点值,比如根节点在第一层,根的左右孩子在第二层,以此类推。
咱们来看看带注释的代码:
ini
def levelOrder(root):
# 如果根节点为空,直接返回空列表
if not root:
return []
# 用于存储最终结果的列表
result = []
# 初始化队列,将根节点加入队列
queue = [root]
# 当队列不为空时,持续处理
while queue:
# 记录当前层的节点数量
level_size = len(queue)
# 用于存储当前层节点值的列表
current_level = []
# 循环处理当前层的所有节点
for _ in range(level_size):
# 从队列头部取出一个节点(队列先进先出)
node = queue.pop(0)
# 将当前节点的值加入当前层的列表
current_level.append(node.val)
# 如果当前节点有左孩子,将其加入队列(下一层节点)
if node.left:
queue.append(node.left)
# 如果当前节点有右孩子,将其加入队列(下一层节点)
if node.right:
queue.append(node.right)
# 将当前层的结果加入最终结果列表
result.append(current_level)
# 返回最终的层序遍历结果
return result
再看一道稍微复杂点的,力扣 200 题 "岛屿数量"。这题是说给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,让你计算岛屿的数量。岛屿是由相邻的陆地连接形成的,而且四周都是水,每个格子只和上下左右四个方向相邻。
带注释的代码如下:
ini
def numIslands(grid):
# 如果网格为空或者网格的第一行为空,返回0
if not grid or not grid[0]:
return 0
# 获取网格的行数和列数
rows, cols = len(grid), len(grid[0])
# 用于记录岛屿数量
count = 0
# 遍历网格中的每个单元格
for i in range(rows):
for j in range(cols):
# 当遇到陆地('1')时
if grid[i][j] == '1':
# 岛屿数量加1
count += 1
# 初始化队列,将当前陆地坐标加入队列
queue = [(i, j)]
# 将当前陆地标记为已访问(改为'0'),避免重复计算
grid[i][j] = '0'
# 当队列不为空时,继续处理
while queue:
# 从队列头部取出一个坐标
x, y = queue.pop(0)
# 定义上下左右四个方向的偏移量
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
# 计算相邻单元格的坐标
nx, ny = x + dx, y + dy
# 检查相邻单元格是否在网格范围内,且是未访问的陆地
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
# 将相邻陆地标记为已访问
grid[nx][ny] = '0'
# 将相邻陆地坐标加入队列,继续扩散
queue.append((nx, ny))
# 返回岛屿的总数量
return count
还有力扣 994 题 "腐烂的橘子",这道题是多源 BFS 的典型应用。题目是说,在给定的 m x n 网格中,每个单元格可以是新鲜橘子(1)、腐烂的橘子(2)或空(0)。腐烂的橘子会在每分钟内使相邻四个方向上的新鲜橘子腐烂。求直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 - 1。
带注释的代码如下:
ini
def orangesRotting(grid):
# 获取网格的行数和列数
rows, cols = len(grid), len(grid[0])
# 初始化队列,用于存储腐烂橘子的坐标
queue = []
# 记录新鲜橘子的数量
fresh = 0
# 遍历网格,找出初始的腐烂橘子和新鲜橘子
for i in range(rows):
for j in range(cols):
if grid[i][j] == 2:
# 将腐烂橘子的坐标加入队列
queue.append((i, j))
elif grid[i][j] == 1:
# 统计新鲜橘子数量
fresh += 1
# 如果没有新鲜橘子,直接返回0
if fresh == 0:
return 0
# 定义上下左右四个方向的偏移量
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# 记录经过的分钟数
minutes = 0
# 当队列不为空时,继续处理
while queue:
# 记录当前队列的长度(即当前分钟要处理的腐烂橘子数量)
size = len(queue)
# 标记当前分钟是否有新鲜橘子被腐烂
has_rotten = False
# 处理当前分钟的所有腐烂橘子
for _ in range(size):
# 从队列头部取出一个腐烂橘子的坐标
x, y = queue.pop(0)
# 检查四个方向的相邻单元格
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 检查相邻单元格是否在网格范围内,且是新鲜橘子
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 1:
# 将新鲜橘子变为腐烂橘子
grid[nx][ny] = 2
# 新鲜橘子数量减1
fresh -= 1
# 将新腐烂的橘子坐标加入队列
queue.append((nx, ny))
# 标记当前分钟有橘子被腐烂
has_rotten = True
# 如果当前分钟有橘子被腐烂,分钟数加1
if has_rotten:
minutes += 1
# 如果还有新鲜橘子没被腐烂,返回-1,否则返回分钟数
return -1 if fresh > 0 else minutes
这三道题虽然场景不同,但都围绕着 BFS 的核心思想展开。二叉树的层序遍历是基础的层级遍历,岛屿数量是通过 BFS 来标记连通区域,腐烂的橘子则是多源 BFS,从多个起点同时开始扩散。