🔎 BFS 与 DFS 图搜索详解

队列 vs 栈:无权图最短路、层序遍历、连通性与拓扑

一句话总结

BFS 和 DFS 是遍历图 / 树 / 网格的两种基本策略。唯一差别是用队列还是用栈来决定"下一个访问谁":

🎢

BFS(广度优先)

用队列,一圈一圈向外扩散。首次到达某点的路径就是最短路径(边权相同时)。

🕳️

DFS(深度优先)

用栈 / 递归,一条路走到黑再回头。擅长找连通块、判环、拓扑排序。

🚫

visited 集合

两者都必须有。图有环,不标记已访问就会无限循环 / 指数级重复。

⚖️

复杂度

时间 O(V + E),空间 O(V)。V 顶点数、E 边数。

一、遍历顺序对比

同一个图(起点 A),两种访问顺序 BFS:一圈一圈扩散(队列) A① B② C② D② E③ F③ G③ 第 1 层:A | 第 2 层:B C D | 第 3 层:E F G 距离起点的层数 = 最短跳数 DFS:一条路走到底(栈/递归) A① B② E③ F④ C⑤ D⑥ A → B → E(到底)→ 回退 → F → 回退 → C → D 走过的路径不保证最短,但能深入探索一条完整分支 BFS 像水波扩散(横着扫)| DFS 像走迷宫(竖着钻) 两者访问节点数相同 O(V+E),只是顺序与"记忆方式"不同

二、代码模板

2.1 BFS:队列 + visited(无权图最短路)

from collections import deque def bfs(graph, start): visited = {start} q = deque([start]) dist = {start: 0} # 记录到起点的最短距离 while q: node = q.popleft() # 队首出队(先来的先扩散) for nxt in graph[node]: if nxt not in visited: visited.add(nxt) # ★ 入队时就标记,防止重复入队 dist[nxt] = dist[node] + 1 q.append(nxt) return dist # 层序遍历写法(需要区分"第几层"时) def bfs_level(graph, start): visited, q, level = {start}, deque([start]), 0 while q: for _ in range(len(q)): # 一次性处理完当前这一层 node = q.popleft() for nxt in graph[node]: if nxt not in visited: visited.add(nxt) q.append(nxt) level += 1 return level
💡 关键细节:必须在入队时就加入 visited,而不是出队时。否则同一节点可能被多个前驱重复入队多次,队列膨胀,复杂度退化。

2.2 DFS:递归 / 显式栈

# 写法一:递归(最简洁,本质是系统调用栈) def dfs_recursive(graph, node, visited): visited.add(node) for nxt in graph[node]: if nxt not in visited: dfs_recursive(graph, nxt, visited) # 写法二:显式栈(避免递归过深爆栈) def dfs_iterative(graph, start): visited, stack = set(), [start] while stack: node = stack.pop() # 栈顶出栈(后入先出 → 往深处钻) if node in visited: continue visited.add(node) for nxt in graph[node]: if nxt not in visited: stack.append(nxt) return visited
⚠️ 递归 DFS 的栈溢出:Python 默认递归深度约 1000。图/网格规模大时(如 300×300 的岛屿题)会 RecursionError,此时改用显式栈迭代或 sys.setrecursionlimit(10000)。

三、网格(矩阵)上的 BFS/DFS

3.1 岛屿问题(LeetCode 200):DFS 洪水填充

数岛屿:遇到陆地 '1' 就 DFS/BFS 把整片相连的陆地"淹掉" 1 1 0 0 1 1 0 0 0 0 1 0 0 0 0 1 岛 ① (4格) 岛 ②(1格) 岛 ③(1格) 扫描整个网格 → 每遇到未访问的 '1',答案 +1,并从它开始 DFS 把连通的 '1' 全部标记访问 时间复杂度 O(m×n):每个格子最多被访问一次
def numIslands(grid): if not grid: return 0 m, n = len(grid), len(grid[0]) count = 0 def dfs(r, c): if (r < 0 or r >= m or c < 0 or c >= n or grid[r][c] != '1'): # 越界 or 非陆地 → 返回 return grid[r][c] = '0' # ★ 就地标记:把陆地"淹成"水,省去 visited 数组 for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)): # 四个方向 dfs(r + dr, c + dc) for r in range(m): for c in range(n): if grid[r][c] == '1': count += 1 dfs(r, c) # 淹掉整座岛 return count

四、怎么选:BFS 还是 DFS?

场景推荐理由
无权图最短路径 / 最少步数BFS首次到达即最短,层数就是距离
二叉树层序遍历BFS按层输出天然契合队列
数连通块 / 岛屿 / 洪水填充DFS(或 BFS 皆可)只需遍历完整块,不关心距离
判环 / 拓扑排序DFS需要"后序遍历"和递归栈判断回边
找所有路径 / 排列组合DFS(回溯)需要完整走完一条路再回退
矩阵最长递增路径DFS + 记忆化需要子问题结果,DFS 天然支持
迷宫最短步数 / 单词接龙BFS求最少变换次数
一句话决策:问"最少几步 / 最短多远 / 按层输出" → BFS;问"是否存在 / 有多少个 / 找出所有方案 / 拓扑关系" → DFS。

4.1 复杂度对比

维度BFSDFS
数据结构队列(FIFO)栈(LIFO)或递归
时间复杂度O(V + E)O(V + E)
空间复杂度O(V)(队列最宽一层的规模)O(V)(递归深度,最坏等于图高)
最短路保证✅ 无权图最短路❌ 不保证
内存峰值稠密图时队列可能很大深图时栈可能很深