BFS 和 DFS 编程思想、框架、技巧及经典例题总结
一、核心编程思想
- BFS(广度优先搜索)
- 核心思想:以「层次遍历」为核心,从起点出发,优先探索距离起点最近的节点,逐层扩散遍历。
- 本质:通过「队列」实现 “先入先出” 的顺序,保证每次处理的都是当前层的所有节点,适合解决「最短路径」「层次遍历」「连通性判断(无权图)」等问题。
- 特点:
- 遍历过程中节点的 “距离”(步数)是递增的,首次到达目标节点时的路径即为最短路径(无权图 / 等权图)。
- 空间复杂度较高(需存储当前层所有节点),但时间复杂度稳定。
- DFS(深度优先搜索)
- 核心思想:以「深度优先」为核心,从起点出发,沿着一条路径深入到底,直到无法前进时回溯,换另一条路径继续探索。
- 本质:通过「递归栈」或「手动栈」实现 “后入先出” 的顺序,适合解决「连通性判断」「排列组合」「路径搜索」「回溯剪枝」等问题。
- 特点:
- 遍历过程具有 “回溯” 特性,可通过剪枝优化无效路径,减少计算量。
- 空间复杂度由递归深度(或栈深度)决定,可能因深度过大导致栈溢出(需注意递归边界)。
二、通用编程框架
- BFS 框架(模板)
基础模板(树 / 图通用)def bfs(start, target):# 初始化队列(存储待处理节点)from collections import dequequeue = deque([start])# 初始化visited(避免重复访问,图必备;树可省略,但需处理父节点回退)visited = set([start])# 记录距离/层数(可选,按需添加)distance = 0 while queue:# 处理当前层的所有节点(关键:先获取当前层节点数)level_size = len(queue)for _ in range(level_size):# 弹出队首节点current = queue.popleft()# 若找到目标,返回结果if current == target:return distance # 或其他结果# 遍历当前节点的所有相邻节点for neighbor in get_neighbors(current):if neighbor not in visited:visited.add(neighbor)queue.append(neighbor)# 层数/距离+1(每处理完一层递增)distance += 1# 未找到目标return -1
- 关键组件:
- 队列:存储待处理的节点,保证 “先入先出”。
- visited 集合:标记已访问节点(图中必用,避免环导致的死循环;树中可省略,但需通过父节点指针避免回退)。
- 层次处理:通过level_size控制每层节点的批量处理,实现 “层次遍历”。
- 关键组件:
- DFS 框架(模板)
递归版(简洁,适合深度不太大的场景)
迭代版(手动栈,适合深度较大的场景,避免递归栈溢出)def dfs(current, visited, target):# 终止条件:找到目标或越界if current == target:return True # 或记录路径if current is invalid: # 剪枝return False# 标记当前节点为已访问visited.add(current)# 遍历所有相邻节点for neighbor in get_neighbors(current):if neighbor not in visited:# 递归探索下一层if dfs(neighbor, visited, target):return True # 找到目标,提前返回# 回溯:若需恢复状态(如排列组合问题),此处可移除标记# visited.remove(current) # 视问题而定是否需要return False # 未找到目标
def dfs_iterative(start, target):stack = [start] # 手动栈visited = set([start])while stack:current = stack.pop() # 弹出栈顶节点(最后入栈的节点)# 找到目标,返回结果if current == target:return True# 遍历相邻节点(注意:栈是后入先出,若需按顺序遍历,可反向添加)for neighbor in reversed(get_neighbors(current)):if neighbor not in visited:visited.add(neighbor)stack.append(neighbor)return False
- 关键组件:
- 栈:递归时隐式使用函数调用栈,迭代时手动维护栈,保证 “后入先出”。
- visited 集合:同 BFS,避免重复访问。
- 回溯:在排列组合等问题中,需在递归返回后移除visited标记,允许节点被其他路径重新访问。
- 关键组件:
三、实用技巧
- BFS 技巧
- 双向 BFS 优化:当起点和终点明确时(如最短路径问题),可从起点和终点同时开始 BFS,相遇时终止,大幅减少搜索空间(适用于节点数量庞大的场景,如「打开转盘锁」)。
- 距离记录:通过数组或哈希表记录每个节点到起点的距离,无需额外变量(如distance[node] = distance[current] + 1)。
- 层次信息利用:在层次遍历问题中(如二叉树每层节点之和),通过level_size区分不同层,方便按层处理数据。
- DFS 技巧
- 剪枝优化:在递归过程中提前判断无效路径(如 “和超过目标的组合”“已存在重复的排列”),直接返回,减少计算量。
- 记忆化搜索:对重复子问题(如动态规划中的递归场景),用哈希表记录已计算的结果,避免重复计算(如「斐波那契数列」「最长递增子序列」的递归实现)。
- 路径跟踪:在需要记录具体路径的问题中(如「所有可能的路径」),通过列表在递归时添加节点,回溯时移除节点,动态维护路径。
四、经典例题解析
- BFS 经典例题
例题 1:二叉树的层序遍历(LeetCode 102)- 问题:给定二叉树的根节点,返回按层序遍历的节点值(逐层从左到右)。
- 思路:用 BFS 按层处理节点,每层遍历前记录队列长度(当前层节点数),依次弹出并收集值,再将子节点入队。
- 代码:
例题 2:打开转盘锁(LeetCode 752)def levelOrder(root):if not root:return []from collections import dequequeue = deque([root])result = []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
- 问题:转盘锁有 4 个数字(0-9),每次可转动一个数字 ±1(如 0→9 或 0→1)。给定死亡数字列表(不可经过),求从 “0000” 到目标数字的最少步数,无法到达则返回 - 1。
- 思路:BFS 求最短路径,每次转动生成 8 种可能的下一个状态(4 个位置,每个 ±1),用 visited 和死亡列表过滤无效状态,- 双向 BFS 可优化效率。
- 核心代码:
def openLock(deadends, target):from collections import dequedead = set(deadends)start = "0000"if start in dead:return -1if start == target:return 0# BFS初始化queue = deque([start])visited = set([start])steps = 0while queue:steps += 1level_size = len(queue)for _ in range(level_size):current = queue.popleft()# 生成所有可能的下一个状态for i in range(4):for d in (-1, 1):# 转动第i位数字(0-9循环)new_digit = (int(current[i]) + d) % 10next_str = current[:i] + str(new_digit) + current[i+1:]if next_str == target:return stepsif next_str not in visited and next_str not in dead:visited.add(next_str)queue.append(next_str)return -1
- DFS 经典例题
例题 1:岛屿数量(LeetCode 200)- 问题:给定二维网格,‘1’ 表示陆地,‘0’ 表示水,求岛屿数量(相邻陆地相连,上下左右为相邻)。
- 思路:遍历网格,遇到未访问的 ‘1’ 时启动 DFS,将所有相连的 ‘1’ 标记为已访问(避免重复计数),每启动一次 DFS 即增加一个岛屿。也可以使用BFS。
- 代码:
例题 2:全排列(LeetCode 46)def numIslands(grid):if not grid:return 0rows, cols = len(grid), len(grid[0])visited = [[False for _ in range(cols)] for _ in range(rows)]count = 0def dfs(i, j):# 越界或非陆地或已访问,直接返回if i < 0 or i >= rows or j < 0 or j >= cols or grid[i][j] == '0' or visited[i][j]:returnvisited[i][j] = True # 标记为已访问# 递归探索上下左右dfs(i+1, j)dfs(i-1, j)dfs(i, j+1)dfs(i, j-1)for i in range(rows):for j in range(cols):if grid[i][j] == '1' and not visited[i][j]:count += 1dfs(i, j) # 标记整个岛屿return count
- 问题:给定不含重复数字的数组,返回所有可能的全排列。
- 思路:DFS 回溯法,通过递归探索每个位置的可能数字,用 visited 标记已使用的数字,回溯时恢复状态以尝试其他组合。
- 代码:
def permute(nums):result = []n = len(nums)def backtrack(path, visited):# 终止条件:路径长度等于数组长度if len(path) == n:result.append(path.copy())returnfor i in range(n):if not visited[i]:# 选择当前数字visited[i] = Truepath.append(nums[i])# 递归下一层backtrack(path, visited)# 回溯:撤销选择path.pop()visited[i] = Falsebacktrack([], [False]*n)return result
五、BFS 与 DFS 的区别与适用场景
维度 | BFS | DFS |
---|---|---|
数据结构 | 队列(先入先出) | 栈 / 递归栈(后入先出) |
适用场景 | 最短路径(无权图)、层次遍历、拓扑排序 | 排列组合、连通性、回溯剪枝、路径搜索 |
空间复杂度 | 较高(取决于最宽层节点数) | 较高(取决于递归深度) |
特点 | 首次到达目标即最短路径 | 可通过回溯剪枝优化效率 |
通过掌握上述框架和技巧,可快速解决大部分 BFS/DFS 相关问题。核心是理解 “层次扩散” 与 “深度探索 + 回溯” 的本质,并根据问题场景选择合适的算法。