1. 从迷宫问题看DFS与BFS的本质差异
去年在准备算法面试时,我遇到了一个经典的迷宫问题:给定一个二维矩阵表示迷宫,其中0代表通路,1代表墙壁,从左上角出发,判断能否到达右下角。当我分别用DFS和BFS实现后,发现两种算法在解决思路上有着根本性的不同。
1.1 DFS:深入探索的单行道思维
深度优先搜索(DFS)就像是一个固执的探险家,它会选择一条路径一直走到底,直到碰壁才回头。在Python中实现DFS时,我通常会使用递归的方式:
python复制def dfs(maze, x, y, visited):
if x < 0 or x >= len(maze) or y < 0 or y >= len(maze[0]) or maze[x][y] == 1 or visited[x][y]:
return False
if x == len(maze)-1 and y == len(maze[0])-1:
return True
visited[x][y] = True
return (dfs(maze, x+1, y, visited) or
dfs(maze, x-1, y, visited) or
dfs(maze, x, y+1, visited) or
dfs(maze, x, y-1, visited))
这种实现方式简洁明了,但实际使用时我发现几个关键点:
- 必须维护visited数组避免重复访问
- 递归深度可能引发栈溢出(Python默认递归深度约1000层)
- 找到的路径不一定是最短路径
1.2 BFS:层层推进的团队协作
相比之下,广度优先搜索(BFS)更像是一个有组织的搜索队,它会同时探索所有可能的路径,并保证第一次到达终点时走过的路径是最短的。Python中通常用队列实现:
python复制from collections import deque
def bfs(maze):
if not maze or maze[0][0] == 1:
return False
rows, cols = len(maze), len(maze[0])
queue = deque([(0, 0)])
maze[0][0] = 1 # 标记为已访问
directions = [(1,0), (-1,0), (0,1), (0,-1)]
while queue:
x, y = queue.popleft()
if x == rows-1 and y == cols-1:
return True
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols and maze[nx][ny] == 0:
queue.append((nx, ny))
maze[nx][ny] = 1
return False
在实际编码比赛中,我发现BFS有几个优势:
- 天然保证找到最短路径
- 使用队列而非递归,避免栈溢出
- 可以方便地记录路径长度
关键经验:当问题要求"最短路径"时,BFS通常是更好的选择;当需要遍历所有可能或空间有限时,DFS可能更合适。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法核心:理解两种搜索的底层机制
2.1 DFS的递归与回溯机制
DFS本质上利用了栈的LIFO(后进先出)特性。在递归实现中,函数调用栈隐式地充当了这个栈的角色。我曾在解决"全排列"问题时深刻体会到这一点:
python复制def permute(nums):
def backtrack(path, used):
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if not used[i]:
used[i] = True
path.append(nums[i])
backtrack(path, used)
path.pop()
used[i] = False
res = []
backtrack([], [False]*len(nums))
return res
这种"尝试-回溯"的模式是DFS的典型特征。在实际应用中,我发现有几个优化点:
- 提前剪枝可以减少不必要的递归
- 适当调整递归顺序可能大幅提升效率
- 对于大型问题,可以改用显式栈实现迭代DFS
2.2 BFS的队列与层次遍历
BFS则利用了队列的FIFO(先进先出)特性,这在树的层次遍历中表现得尤为明显。我在解决二叉树右视图问题时,使用了标准的BFS模板:
python复制from collections import deque
def rightSideView(root):
if not root:
return []
queue = deque([root])
res = []
while queue:
level_size = len(queue)
for i in range(level_size):
node = queue.popleft()
if i == level_size - 1:
res.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return res
这个模板可以解决许多层次相关问题,如:
- 二叉树的最小深度
- 二叉树的锯齿形遍历
- 岛屿数量问题
实用技巧:在BFS中记录当前层次大小(level_size)是处理层次相关问题的关键。
3. 典型应用场景与LeetCode实战
3.1 DFS的经典应用场景
- 连通性问题:如岛屿数量(LeetCode 200)
python复制def numIslands(grid):
def dfs(i, j):
if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]) or grid[i][j] != '1':
return
grid[i][j] = '0'
dfs(i+1,j); dfs(i-1,j); dfs(i,j+1); dfs(i,j-1)
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
dfs(i,j)
count += 1
return count
- 排列组合问题:如子集(LeetCode 78)
python复制def subsets(nums):
def backtrack(start, path):
res.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i+1, path)
path.pop()
res = []
backtrack(0, [])
return res
- 记忆化DFS:如爬楼梯(LeetCode 70)
python复制def climbStairs(n):
memo = {}
def dfs(step):
if step in memo:
return memo[step]
if step > n:
return 0
if step == n:
return 1
memo[step] = dfs(step+1) + dfs(step+2)
return memo[step]
return dfs(0)
3.2 BFS的经典应用场景
- 最短路径问题:如打开转盘锁(LeetCode 752)
python复制def openLock(deadends, target):
dead = set(deadends)
if "0000" in dead:
return -1
queue = deque([("0000", 0)])
visited = {"0000"}
while queue:
current, steps = queue.popleft()
if current == target:
return steps
for i in range(4):
for delta in [-1, 1]:
new_digit = (int(current[i]) + delta) % 10
new_state = current[:i] + str(new_digit) + current[i+1:]
if new_state not in visited and new_state not in dead:
visited.add(new_state)
queue.append((new_state, steps+1))
return -1
- 层次遍历问题:如二叉树的最小深度(LeetCode 111)
python复制def minDepth(root):
if not root:
return 0
queue = deque([(root, 1)])
while queue:
node, depth = queue.popleft()
if not node.left and not node.right:
return depth
if node.left:
queue.append((node.left, depth+1))
if node.right:
queue.append((node.right, depth+1))
- 多源BFS:如腐烂的橘子(LeetCode 994)
python复制def orangesRotting(grid):
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
time = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh += 1
directions = [(1,0), (-1,0), (0,1), (0,-1)]
while queue and fresh > 0:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0<=nr<rows and 0<=nc<cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
queue.append((nr, nc))
time += 1
return time if fresh == 0 else -1
4. 性能优化与常见陷阱
4.1 DFS的优化策略
- 剪枝优化:在解决数独问题时,提前终止不可能的分支
python复制def solveSudoku(board):
def is_valid(x, y, num):
for i in range(9):
if board[i][y] == num or board[x][i] == num:
return False
box_x, box_y = x//3*3, y//3*3
for i in range(3):
for j in range(3):
if board[box_x+i][box_y+j] == num:
return False
return True
def dfs():
for i in range(9):
for j in range(9):
if board[i][j] == '.':
for num in '123456789':
if is_valid(i, j, num):
board[i][j] = num
if dfs():
return True
board[i][j] = '.'
return False
return True
dfs()
- 记忆化搜索:如青蛙过河问题(LeetCode 403)
python复制def canCross(stones):
memo = {}
stone_set = set(stones)
def dfs(pos, jump):
if (pos, jump) in memo:
return memo[(pos, jump)]
if pos == stones[-1]:
return True
for j in [jump-1, jump, jump+1]:
if j > 0 and pos + j in stone_set:
if dfs(pos + j, j):
memo[(pos, jump)] = True
return True
memo[(pos, jump)] = False
return False
return dfs(0, 0)
4.2 BFS的优化策略
- 双向BFS:显著减少搜索空间,如单词接龙(LeetCode 127)
python复制def ladderLength(beginWord, endWord, wordList):
if endWord not in wordList:
return 0
wordSet = set(wordList)
begin_queue = {beginWord}
end_queue = {endWord}
visited = set()
length = 1
while begin_queue and end_queue:
if len(begin_queue) > len(end_queue):
begin_queue, end_queue = end_queue, begin_queue
next_queue = set()
for word in begin_queue:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in end_queue:
return length + 1
if next_word in wordSet and next_word not in visited:
visited.add(next_word)
next_queue.add(next_word)
begin_queue = next_queue
length += 1
return 0
- A*搜索:结合启发式函数的BFS优化
python复制import heapq
def aStarSearch(start, goal, graph):
open_set = []
heapq.heappush(open_set, (0 + heuristic(start, goal), 0, start))
came_from = {}
g_score = {start: 0}
while open_set:
_, current_g, current = heapq.heappop(open_set)
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1]
for neighbor in graph[current]:
tentative_g = current_g + distance(current, neighbor)
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor, goal)
heapq.heappush(open_set, (f_score, tentative_g, neighbor))
return []
4.3 常见错误与调试技巧
- DFS中的常见错误:
- 忘记维护visited状态导致无限循环
- 递归终止条件不完整
- 回溯时状态恢复不完全
调试方法:
- 打印递归树和当前状态
- 使用小规模测试用例
- 添加递归深度限制
- BFS中的常见错误:
- 队列中存储的信息不足(如缺少步数记录)
- 层次遍历时未正确处理level_size
- 过早标记为已访问导致错过更优路径
调试方法:
- 打印每层的队列状态
- 可视化搜索过程
- 检查边界条件处理
实战建议:在解决新问题时,先用小规模测试手动模拟DFS/BFS过程,确保完全理解算法行为后再编码。
