走迷宫的两种走法:广度优先(BFS)一圈一圈向外扩散(用队列,天然找【最短】);深度优先(DFS)一条路走到黑、碰壁回头(用栈,省内存、找【所有解】);剪枝=走到一半发现【这条路注定白走】就提前回头——搜索的三件套。
3-17 的树是【场地】,本篇是【走法】。回收 3-16 的队列与栈——BFS 用队列、DFS 用栈,两种数据结构决定了两种走法,3-16 的伏笔在此兑现。
一、BFS:层层扩散,天然最短
从起点出发,先把所有一步能到的访问完,再访问两步能到的——像水面涟漪一圈圈扩:
from collections import deque
graph = { # 邻接表:谁连着谁
"A": ["B", "C"],
"B": ["A", "D", "E"],
"C": ["A", "F"],
"D": ["B"],
"E": ["B", "F"],
"F": ["C", "E"],
}
def bfs(start, target):
visited = {start}
q = deque([(start, [start])]) # 队列装【节点 + 走过来的路径】
while q:
node, path = q.popleft() # 先进先出——所以先访完近的
if node == target:
return path # 第一次命中 = 步数最少
for nxt in graph[node]:
if nxt not in visited:
visited.add(nxt)
q.append((nxt, path + [nxt]))
print(bfs("A", "F")) # ['A', 'C', 'F'] —— 2 步,绕开 D/E 的长路
为什么 BFS 找最短:队列先进先出——【离起点 2 步的】一定在【3 步的】之前被访问,第一次撞见目标时路径必然最少。地图导航、社交网络【几度好友】、走迷宫最短路全是它。
二、DFS:一条道走到黑,回头再探
能深入就深入,走投无路退一步换条岔路——用栈(或递归,函数调用栈天然就是):
def dfs(node, target, visited=None, path=None):
if visited is None:
visited, path = set(), []
visited.add(node)
path.append(node)
if node == target:
return path
for nxt in graph[node]:
if nxt not in visited:
r = dfs(nxt, target, visited, path) # 递归深入(栈就是调用栈)
if r:
return r
return None
print(dfs("A", "F")) # ['A', 'B', 'D', 'E', 'F'] —— 含回头走过的 D,不保证最短
性格:不保证最短(先深入哪条看运气);内存省(只记当前一条路);找【所有解】的主场——数独穷举、全排列、文件树遍历(4-7 递归的主战场)。
三、剪枝:注定白走就别走
搜索树大到爆炸时(O(2ⁿ) 级),走到一半就能判断【再走也没戏】——立刻回头,把整棵子树剪掉:
def subset_sum(nums, target, i=0, acc=0):
if acc == target:
return True
if acc > target or i == len(nums): # 剪枝:加超了 / 没数了 —— 整棵子树不走了
return False
return (subset_sum(nums, target, i + 1, acc + nums[i]) # 要第 i 个
or subset_sum(nums, target, i + 1, acc)) # 不要
print(subset_sum([3, 34, 4, 12, 5, 2], 9)) # True(4+5)
两类常用剪枝:可行性剪枝(当前已超限——如 acc > target,继续只会更超);重复剪枝(访问过的状态不再进——BFS 的 visited 就是它)。AI 对弈(alpha-beta 剪枝)、数独求解全靠它把天文数字的搜索树砍到能跑。
四、动手 Demo:两种走法的答案对比
# graph 沿用上文
print(bfs("A", "F")) # ['A', 'C', 'F'] —— 最短 2 步
print(dfs("A", "F")) # ['A', 'B', 'D', 'E', 'F'] —— 4 节点含回头(先深入 B 支路)
看到什么算成功:两条不同路径,BFS 的更短。
读结果:同图同目标,BFS 给最短路(队列的功劳);DFS 的 path 记的是【走过的全部顺序】——D 是条死胡同但也在序列里(走到了才发现),找到的是【先撞见的那条】。要最短用 BFS、要全解用 DFS——口诀就这一句。
五、30 秒实验
把 graph 里 E 到 F 的连线删掉再跑 bfs("A", "F")——返回 ['A', 'C', 'F'] 不变;把 C 到 F 也删掉——返回 None(此路不通)。30 秒验证图结构一改、搜索立刻给出新答案——算法是通用发动机,图才是图纸。
六、常见误区
- 【BFS 和 DFS 只是遍历顺序不同,没本质区别】——错。队列 vs 栈的结构差异带来能力差异:BFS 天然最短路、DFS 天然省内存——数据结构决定算法能力(3-16 的中心思想)。
- 【BFS 一定比 DFS 慢】——分场景。找最近解 BFS 快(不用走深);找【很深处】的解或枚举全部,DFS 少占内存还能配合剪枝——没有万能选手。
- 【visited 可加可不加】——图里有环时必须加:没 visited,A→B→A→B 永远转圈(BFS 死循环、DFS 爆栈)——树没环可省,图必须有。
自测三问(点开对答案)
1. **BFS 和 DFS 各用什么数据结构?各带来什么能力?** 答:BFS 用队列(先进先出)——近的先访问,天然最短路;DFS 用栈(后进先出,递归即调用栈)——一路到底,省内存、便枚举。 2. **为什么 BFS 第一次命中就是最短路径?** 答:队列保证按离起点的步数分层访问——2 步的节点全访完才轮到 3 步的,首次命中时步数必然最少。 3. **剪枝的原理是什么?举一类。** 答:搜索中途判断【该分支不可能出解】就整棵子树放弃——如子集和问题里当前和已超目标还继续加,可行性剪枝直接回头。- ☐ 下一节(4-17 字符串查找、蛮力法、BM算法)
相关笔记
- 循环 —— 队列与栈在此上岗
- 堆栈与队列 —— 两种走法的发动机
- 树形结构 —— 遍历的场地
- ⌨️ 编程原理 · 索引 —— 全课路线图