news 2026/9/19 17:37:42

BFS算法详解:从原理到实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BFS算法详解:从原理到实战应用

1. 广度优先搜索(BFS)算法概述

广度优先搜索(Breadth-First Search)是图论中最基础的遍历算法之一,也是解决许多实际问题的利器。我第一次接触BFS是在大学的数据结构课上,当时就被它那种"层层递进"的搜索方式所吸引。与深度优先搜索(DFS)不同,BFS总是优先访问离起点最近的节点,这种特性使得它在寻找最短路径、层级遍历等问题上具有天然优势。

BFS的核心思想可以用一个简单的比喻来理解:想象你向平静的湖面投入一颗石子,水波会以石子落点为中心,一圈一圈地向外扩散。BFS就是这样,从起点开始,先访问所有直接相邻的节点(第一层),然后是这些节点的相邻节点(第二层),以此类推,直到找到目标或遍历完整个图。

在实际应用中,BFS常用于解决以下几类问题:

  • 无权图的最短路径问题
  • 状态空间搜索(如迷宫问题、八数码问题)
  • 社交网络中的关系度计算
  • 树或图的层级遍历
  • 连通性问题

提示:BFS特别适合解决"最少步数"、"最短距离"这类问题,因为它的遍历顺序天然保证了第一次到达目标时的路径就是最短的。

2. BFS算法模板详解

2.1 基础BFS模板代码

下面是一个通用的BFS模板,适用于大多数场景。我们以Python为例:

from collections import deque def bfs(start, target): # 初始化队列和已访问集合 queue = deque([start]) visited = set([start]) step = 0 # 记录扩散的步数 while queue: # 当前层的节点数量 size = len(queue) # 遍历当前层的所有节点 for _ in range(size): node = queue.popleft() # 判断是否到达目标 if node == target: return step # 将相邻节点加入队列 for neighbor in get_neighbors(node): if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) # 当前层遍历完成,步数加1 step += 1 # 未找到目标 return -1

这个模板包含了BFS的几个关键要素:

  1. 使用队列(deque)来存储待访问节点
  2. 使用集合(set)记录已访问节点,避免重复访问
  3. 分层遍历(通过size变量控制)
  4. 步数统计(step变量)

2.2 模板各组件解析

队列的选择:Python中推荐使用collections.deque而不是list,因为deque在头部删除元素的时间复杂度是O(1),而list是O(n)。对于大规模数据,这个差异会非常明显。

访问标记visited集合用于防止重复访问和循环。在某些问题中,我们可以用其他方式替代,比如直接修改输入数据(如将访问过的格子标记为障碍物)。

分层遍历:通过size = len(queue)for _ in range(size)的组合,我们可以精确控制当前层的遍历范围。这在需要知道遍历层数(如最短步数)的问题中特别有用。

邻居获取get_neighbors函数需要根据具体问题实现。它定义了从当前节点可以到达哪些节点,这是BFS应用于不同问题的关键适配点。

2.3 模板的常见变体

根据问题特点,BFS模板可以有多种变体:

  1. 双向BFS:同时从起点和终点开始搜索,当两个搜索相遇时停止。适用于起点和终点都明确的情况,可以显著减少搜索空间。

  2. 多源BFS:初始时队列中包含多个起点。适用于如"多个污染源同时扩散"这类场景。

  3. 优先队列BFS:使用优先队列代替普通队列,实际上就变成了Dijkstra算法,适用于带权图的最短路径问题。

  4. 层级信息记录:有时需要记录每个节点的父节点或到达路径,可以在visited中使用字典代替集合。

3. BFS经典题目解析

3.1 二叉树层级遍历(LeetCode 102)

这是最基础的BFS应用,直接套用模板即可:

def levelOrder(root): if not root: return [] result = [] queue = deque([root]) while queue: level_size = len(queue) current_level = [] for _ in range(level_size): node = queue.popleft() 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

关键点

  • 树结构无需visited集合,因为树没有环
  • 每层节点值存储在current_level列表中
  • 时间复杂度O(n),空间复杂度O(n)

3.2 迷宫最短路径(LeetCode 490)

这类问题通常给出一个二维矩阵表示迷宫,需要找到从起点到终点的最短路径:

def hasPath(maze, start, destination): m, n = len(maze), len(maze[0]) directions = [(0,1),(1,0),(0,-1),(-1,0)] queue = deque([start]) visited = set([(start[0], start[1])]) while queue: i, j = queue.popleft() if [i, j] == destination: return True for di, dj in directions: ni, nj = i, j # 沿着一个方向滚到不能滚为止 while 0 <= ni + di < m and 0 <= nj + dj < n and maze[ni+di][nj+dj] == 0: ni += di nj += dj if (ni, nj) not in visited: visited.add((ni, nj)) queue.append((ni, nj)) return False

注意事项

  • 迷宫问题中,球会一直滚动直到碰到障碍物,所以内层需要while循环模拟滚动过程
  • 使用directions数组表示四个移动方向,代码更简洁
  • 访问标记要放在停止的位置,而不是每一步

3.3 单词接龙(LeetCode 127)

这个问题要求将一个单词每次改变一个字母,逐步转换为另一个单词,所有中间单词必须在字典中:

def ladderLength(beginWord, endWord, wordList): wordSet = set(wordList) if endWord not in wordSet: return 0 queue = deque([beginWord]) visited = set([beginWord]) changes = 1 while queue: size = len(queue) for _ in range(size): word = queue.popleft() if word == endWord: return changes # 生成所有可能的下一个单词 for i in range(len(word)): for c in 'abcdefghijklmnopqrstuvwxyz': next_word = word[:i] + c + word[i+1:] if next_word in wordSet and next_word not in visited: visited.add(next_word) queue.append(next_word) changes += 1 return 0

优化技巧

  • 使用双向BFS可以显著提高效率
  • 预处理构建单词模式字典可以加速邻居查找
  • 时间复杂度:O(M^2×N),其中M是单词长度,N是字典大小

3.4 岛屿数量(LeetCode 200)

虽然这个问题通常用DFS解决,但BFS也是一种可行方案:

def numIslands(grid): if not grid: return 0 m, n = len(grid), len(grid[0]) count = 0 directions = [(-1,0),(1,0),(0,-1),(0,1)] for i in range(m): for j in range(n): if grid[i][j] == '1': count += 1 queue = deque([(i,j)]) grid[i][j] = '0' # 标记为已访问 while queue: x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == '1': grid[nx][ny] = '0' queue.append((nx, ny)) return count

实现细节

  • 直接在原矩阵上修改,将访问过的'1'改为'0',节省空间
  • 每个未被访问的'1'代表一个新岛屿,启动一次BFS标记所有相连的'1'
  • 时间复杂度O(M×N),空间复杂度O(min(M,N))(队列最大长度)

4. BFS应用中的常见问题与技巧

4.1 如何避免重复访问

重复访问是BFS中最常见的问题之一,会导致无限循环和性能下降。解决方法包括:

  1. 使用集合记录已访问节点:这是最通用的方法,适用于大多数场景。

    visited = set() visited.add(node)
  2. 修改原数据:对于矩阵类问题,可以直接修改原数据表示已访问。

    grid[i][j] = '#' # 标记为已访问
  3. 使用距离数组:在求最短路径时,可以用一个数组记录到每个节点的最短距离,初始为无穷大,只有当找到更短距离时才更新。

    distance = [[float('inf')] * n for _ in range(m)] distance[start[0]][start[1]] = 0

4.2 如何处理大规模状态空间

当状态空间很大时,BFS可能会消耗过多内存。可以考虑以下优化:

  1. 双向BFS:从起点和终点同时开始搜索,相遇时停止。可以将搜索空间减半。

  2. 启发式搜索:如A*算法,使用优先队列并引入启发式函数,优先探索更有可能接近目标的节点。

  3. 状态压缩:对于某些问题,可以将状态表示为更紧凑的形式(如位运算),减少内存占用。

  4. 层级剪枝:如果知道解的最大深度,可以设置最大步数限制。

4.3 如何记录路径

有时不仅需要知道能否到达目标,还需要知道具体路径。可以通过以下方式实现:

  1. 记录父节点:在visited中使用字典而不是集合,保存每个节点的前驱节点。

    parent = {start: None} # 在遍历邻居时 parent[neighbor] = current # 回溯路径 path = [] while node: path.append(node) node = parent[node] path.reverse()
  2. 保存完整路径:直接在队列中存储从起点到当前节点的路径,但这种方法内存消耗较大。

    queue = deque([[start]]) while queue: path = queue.popleft() node = path[-1] # ... queue.append(path + [neighbor])

4.4 性能优化技巧

  1. 尽早终止:一旦找到目标立即返回,避免不必要的搜索。

  2. 预处理:如单词接龙问题中,可以预先构建单词模式字典,加速邻居查找。

  3. 选择合适的队列:Python中deque比list更适合作为队列使用。

  4. 减少不必要的操作:如在矩阵问题中,先检查边界再访问元素,避免异常处理开销。

  5. 并行处理:对于多源BFS,可以考虑并行处理同一层的多个节点(在Python中受GIL限制效果有限)。

5. BFS与其他算法的比较与选择

5.1 BFS vs DFS

BFS和DFS是两种最基本的图遍历算法,各有优缺点:

特性BFSDFS
实现方式队列栈/递归
空间复杂度O(b^d)(b为分支因子,d为深度)O(d)
最短路径天然支持需要额外记录
适用场景最短路径、层级遍历拓扑排序、连通分量、回溯问题
内存消耗通常较大通常较小
实现难度较简单递归实现可能栈溢出

选择原则:

  • 需要最短路径或层级信息 → BFS
  • 图很深或需要遍历所有可能 → DFS
  • 内存受限 → DFS
  • 不知道哪种更好 → 都试试看

5.2 BFS vs Dijkstra

Dijkstra算法是BFS在带权图中的推广:

特性BFSDijkstra
适用图类型无权图或等权图带权图
数据结构队列优先队列
时间复杂度O(V+E)O((V+E)logV)
结果最短跳数最短路径权重和
实现复杂度简单中等

当图中所有边的权重相等时,Dijkstra算法退化为BFS。

5.3 BFS vs A*

A*算法是Dijkstra的改进,加入了启发式函数:

特性BFSA*
启发式
数据结构队列优先队列
搜索方向均匀扩散向目标方向倾斜
适用场景通用有明确目标位置
最优性保证启发式可采纳时保证

A*在知道目标位置且能找到好的启发式函数时,通常比BFS更高效。

6. BFS在实际工程中的应用

6.1 网络爬虫

BFS是网络爬虫的基础算法之一。爬虫从种子URL开始,通过BFS方式遍历网页链接:

def crawl(start_url): visited = set() queue = deque([start_url]) while queue: url = queue.popleft() if url not in visited: visited.add(url) html = download(url) urls = extract_links(html) for new_url in urls: if is_valid(new_url) and new_url not in visited: queue.append(new_url)

注意事项

  • 需要限制爬取深度和总数
  • 需要处理robots.txt
  • 需要考虑域名限制和爬取间隔
  • 分布式爬虫需要共享visited集合

6.2 社交网络分析

在社交网络中,BFS可用于:

  • 计算两个人之间的最短连接路径
  • 查找某人的N度人脉
  • 发现社交群体(结合连通分量算法)
def find_connection(graph, person1, person2): queue = deque([(person1, [person1])]) visited = set([person1]) while queue: person, path = queue.popleft() if person == person2: return path for friend in graph[person]: if friend not in visited: visited.add(friend) queue.append((friend, path + [friend])) return None

6.3 游戏AI

许多游戏中的AI寻路算法基于BFS或其变种:

  • 迷宫类游戏的最短路径计算
  • 策略游戏的战争迷雾探索
  • 解谜游戏的状态空间搜索
def find_path(grid, start, end): # grid是二维矩阵,0表示可通行,1表示障碍 directions = [(0,1),(1,0),(0,-1),(-1,0)] queue = deque([(start, [start])]) visited = set([start]) while queue: (x,y), path = queue.popleft() if (x,y) == end: return path for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): if grid[nx][ny] == 0 and (nx,ny) not in visited: visited.add((nx,ny)) queue.append(((nx,ny), path + [(nx,ny)])) return None

6.4 其他应用场景

  1. 电路设计:PCB布线、最短连线
  2. 图像处理:区域填充、连通区域分析
  3. 推荐系统:基于关系的推荐(如"朋友的朋友")
  4. 自动化测试:UI自动化中的控件遍历
  5. 网络路由:IP包的路由选择

7. BFS的高级应用与扩展

7.1 多源BFS

多源BFS是指从多个起点同时开始的BFS,适用于如"多个污染源同时扩散"这类场景。实现上与普通BFS类似,只是初始队列中包含多个起点:

def multi_source_bfs(sources, grid): m, n = len(grid), len(grid[0]) queue = deque(sources) visited = set(sources) distance = 0 while queue: size = len(queue) for _ in range(size): x, y = queue.popleft() # 处理当前节点 for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and (nx,ny) not in visited: visited.add((nx,ny)) queue.append((nx,ny)) distance += 1 return distance

典型应用:LeetCode 994(腐烂的橘子)、LeetCode 542(01矩阵)

7.2 双向BFS

双向BFS从起点和终点同时开始搜索,当两个搜索相遇时停止。可以显著减少搜索空间:

def bidirectional_bfs(start, end, graph): if start == end: return [start] # 初始化两个队列和访问字典 queue_start = deque([start]) queue_end = deque([end]) visited_start = {start: None} visited_end = {end: None} found = None while queue_start and queue_end: # 从起点方向扩展 found = expand_level(queue_start, visited_start, visited_end, graph) if found: break # 从终点方向扩展 found = expand_level(queue_end, visited_end, visited_start, graph) if found: break # 重构路径 if not found: return None path = [] node = found while node: path.append(node) node = visited_start[node] path.reverse() node = visited_end[found] while node: path.append(node) node = visited_end[node] return path def expand_level(queue, visited, other_visited, graph): size = len(queue) for _ in range(size): node = queue.popleft() for neighbor in graph[node]: if neighbor not in visited: visited[neighbor] = node queue.append(neighbor) if neighbor in other_visited: return neighbor return None

适用条件

  • 起点和终点都明确
  • 可以定义反向的邻居关系
  • 搜索空间较大时效果明显

7.3 带优先级的BFS

当图中边带有不同的权重时,需要使用优先队列(即Dijkstra算法):

import heapq def dijkstra(graph, start): heap = [(0, start)] distances = {start: 0} while heap: dist, node = heapq.heappop(heap) if dist > distances[node]: continue for neighbor, weight in graph[node].items(): new_dist = dist + weight if neighbor not in distances or new_dist < distances[neighbor]: distances[neighbor] = new_dist heapq.heappush(heap, (new_dist, neighbor)) return distances

注意事项

  • 时间复杂度O((V+E)logV)
  • 不能处理负权边(此时应使用Bellman-Ford算法)
  • 当权重均为1时,退化为普通BFS

7.4 状态压缩BFS

某些问题中,状态可以用更紧凑的方式表示以节省空间:

def state_compression_bfs(start): queue = deque([start]) visited = set([start]) while queue: state = queue.popleft() if is_target(state): return True for next_state in generate_next_states(state): compressed = compress(next_state) if compressed not in visited: visited.add(compressed) queue.append(next_state) return False

典型应用:八数码问题、滑块拼图等状态空间搜索问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 17:35:20

Flutter ProgressIndicator 在 OpenHarmony 上的实战:从组件到性能优化

1. 组件认知&#xff1a;ProgressIndicator 解决了什么问题1.1 从用户感知到开发成本的必然选择在 OpenHarmony 生态里做应用开发&#xff0c;加载反馈是躲不开的刚需。页面拉取网络数据、文件读写、图片解码、后台任务提交&#xff0c;这些操作都需要一段时间&#xff0c;如果…

作者头像 李华
网站建设 2026/9/19 17:32:40

Unity真实地形生成:ArcGIS Maps SDK实战与性能优化

1. 为什么要在Unity里折腾真实地形如果你做过数字孪生、飞行模拟、城市规划演示或者户外战术类游戏&#xff0c;一定绕不开一个核心痛点&#xff1a;地形不够真。Unity自带的Terrain系统做个小山包、挖条河还行&#xff0c;但一旦涉及真实地理坐标、大范围地貌、卫星影像贴图&a…

作者头像 李华
网站建设 2026/9/19 17:32:12

二手房数据爬取与结构化工程实践指南

简介&#xff1a;本资源是一份面向专科及本科毕业生的毕业论文范例&#xff0c;聚焦Python网络爬虫在二手房数据采集与可视化分析中的工程实践&#xff0c;解决房源信息分散、难以比价分析的现实问题&#xff0c;兼顾数据挖掘、Django后端开发与可视化技术应用。压缩包含1个28K…

作者头像 李华
网站建设 2026/9/19 17:31:05

JEP106BE制造商识别码实战解析:从PCIe到DDR5的MIC解码与调试

简介&#xff1a;本资源为JEDEC协会2022年发布的JEP106BE标准正式文档&#xff0c;面向半导体设计、芯片采购、FAE支持及电子元器件合规管理相关从业者&#xff0c;解决制造商识别码&#xff08;MID&#xff09;分配不统一、跨厂商产品溯源困难、BOM识别易出错等实际问题。文档…

作者头像 李华
网站建设 2026/9/19 17:25:55

ADS负载牵引实战:射频功放阻抗优化与史密斯圆图深度解读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华