深度优先搜索:从 "一条道走到黑" 到搞定算法题
刷抖音时总刷到 "当代年轻人现状:选了一条路就硬走,撞了南墙才回头"?这不就是深度优先搜索(DFS)的精髓嘛!DFS 这算法跟咱年轻人的 "犟脾气" 如出一辙 ------ 遇到岔路口先闷头往前走,走不通了再退回去换条路,主打一个 "不撞南墙不回头,撞了南墙就回溯"。今天咱就用 Python 代码拆解这算法,看看它咋帮咱搞定力扣难题和面试考点。
啥是 DFS?说白了就是 "死磕到底"
DFS 的核心逻辑特简单:从起点出发,先把一条路走到头,实在没路了就退回到上一个路口,换条路继续死磕。就像玩密室逃脱时,你总不能刚看到个门就掉头吧?不得先进去瞅瞅有没有钥匙嘛!
实现 DFS 有两种姿势:
- 递归版:自带 "后悔药"(函数调用栈帮你记着回头的路)
- 栈版:手动记路(适合怕递归太深 "爆栈" 的场景)
咱先看个二叉树遍历的例子,这是 DFS 的经典秀场。
二叉树遍历:DFS 的 "开胃小菜"
二叉树的前序遍历(根→左→右)就是典型的 DFS 操作。比如给你棵树:
markdown
1
\
2
/
3
按 DFS 的思路,先啃根节点 1,再一头扎进右子树,摸到 2 之后又钻进左子树找到 3,完美符合 "一条道走到黑" 的原则。
递归实现:简单到离谱
python
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def dfs_tree(root):
result = []
def traverse(node):
if not node:
return
# 先拿根节点的value
result.append(node.val)
# 左子树死磕到底
traverse(node.left)
# 右子树死磕到底
traverse(node.right)
traverse(root)
return result
# 测试一下
root = TreeNode(1, right=TreeNode(2, left=TreeNode(3)))
print(dfs_tree(root)) # 输出 [1,2,3],跟预期一模一样
这代码简洁得像抖音神曲,几行就搞定。但如果树太深(比如几万层),递归可能会 "栈溢出",这时候就得用栈手动模拟。
栈实现:手动记路不翻车
ini
def dfs_stack(root):
if not root:
return []
stack = [root]
result = []
while stack:
node = stack.pop() # 弹出栈顶元素
result.append(node.val)
# 右子树先入栈,因为栈是"后进先出",保证左子树先被访问
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
print(dfs_stack(root)) # 同样输出 [1,2,3]
用栈的时候得注意入栈顺序,右子树先塞进去,才能让左子树先被 "啃" 到,这点跟递归版保持一致。
力扣热门题:DFS 实战秀
光说不练假把式,咱拿两道面试高频题练练手,看看 DFS 咋大显神通。
1. 岛屿数量(LeetCode 200):DFS 的 "扫雷" 时刻
这题就像玩扫雷,碰到一个 "1"(陆地),就得把周围所有连在一起的 "1" 都标记成 "0"(水),每扫完一片就是一个岛屿。
python
def numIslands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(i, j):
# 越界或者不是陆地,直接撤退
if i < 0 or i >= rows or j < 0 or j >= cols 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) # 右
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
count += 1
dfs(i, j) # 把整个岛屿都"淹"掉
return count
# 测试用例
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
print(numIslands(grid)) # 输出3,完美!
这题的关键是 "标记已访问",不然会重复计算。就像抖音上的 "打卡",去过的地方得打个勾,免得下次又跑过去。
2. 全排列(LeetCode 46):DFS 的 "排列组合" 小课堂
给一串数字,返回所有可能的排列。比如 [1,2,3],得返回 6 种组合。这题用 DFS + 回溯简直绝配,就像穿衣服 ------ 先穿 1,再试 2,最后套 3;不行就脱了 3 换 2,以此类推。
python
def permute(nums):
result = []
n = len(nums)
def dfs(path, used):
# 路径长度够了,就加入结果
if len(path) == n:
result.append(path.copy())
return
for i in range(n):
if not used[i]:
# 选当前数字
used[i] = True
path.append(nums[i])
# 继续选下一个
dfs(path, used)
# 回溯:把选的数字放回去
path.pop()
used[i] = False
dfs([], [False]*n)
return result
print(permute([1,2,3])) # 输出所有6种排列,一目了然
这里的 "回溯" 就像抖音上的 "后悔药" 特效,选错了立马撤回重来,特方便。
面试常问:DFS 的 "优缺点" 和 "应用场景"
面试官最爱问 "DFS 适合啥场景?""它跟 BFS 有啥区别?",咱得答得像刷抖音刷到的知识点一样顺口:
- 适合的场景:
-
- 迷宫问题(找出口、走迷宫)
-
- 排列组合(全排列、子集问题)
-
- 连通性问题(岛屿数量、朋友圈)
-
- 拓扑排序(课程表问题)
- 优点:代码简单(递归版几行搞定),内存占用比 BFS 少(不用记一整层的节点)。
- 缺点:可能绕远路(找不到最短路径),递归太深容易 "爆栈"(比如处理 10 万层的树)。
记住:求最短路径选 BFS,求所有可能路径选 DFS,就像抖音上的 "选对赛道很重要"。
总结:DFS 就是 "死磕 + 回头" 的艺术
DFS 这算法说难也难,说简单也简单。核心就是 "一条道走到黑,走不通就回头",跟咱生活中试错的过程一模一样。多练几道力扣题(比如子集、电话号码的字母组合),你就会发现:哦,原来 DFS 就是这么回事儿,之前真是想复杂了!
最后送句抖音热评:"递归一时爽,一直递归一直爽,直到栈溢出 ------ 那就换栈实现呗!" 掌握 DFS,算法题从此少掉一把头发~