深度优先搜索(Depth-First Search)是二叉树遍历中最基础也最重要的算法之一。与广度优先搜索(BFS)按层级遍历不同,DFS会沿着一条路径尽可能深入地探索,直到无法继续前进时才回溯。这种"一条路走到黑"的特性,使其在解决某些特定类型问题时具有独特优势。
在实际工程中,DFS常用于:
理解DFS的关键在于掌握其递归本质——将大问题分解为相同结构的子问题。以二叉树为例,处理当前节点后,只需递归处理其左右子树即可。这种"分而治之"的思想,使得DFS代码通常简洁优雅。
递归是DFS最自然的实现方式,代码通常不超过10行。以下是标准的前序遍历递归实现:
python复制def dfs_preorder(root):
if not root:
return
print(root.val) # 处理当前节点
dfs_preorder(root.left) # 递归左子树
dfs_preorder(root.right) # 递归右子树
递归实现的优势在于:
但需要注意递归深度问题。对于极端不平衡的二叉树(如退化为链表),递归可能导致栈溢出。此时需要改用迭代实现或进行尾递归优化。
所有递归算法都可以转换为迭代实现。DFS的迭代版本需要显式使用栈来模拟递归的调用过程:
python复制def dfs_preorder_iterative(root):
if not root:
return
stack = [root]
while stack:
node = stack.pop()
print(node.val) # 处理当前节点
# 注意压栈顺序:先右后左,保证左子树先处理
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
迭代实现的优势:
Morris遍历是一种巧妙的DFS实现,通过临时修改树结构(遍历后恢复)来实现O(1)空间复杂度:
python复制def morris_preorder(root):
curr = root
while curr:
if not curr.left:
print(curr.val)
curr = curr.right
else:
# 找到当前节点的前驱节点
pre = curr.left
while pre.right and pre.right != curr:
pre = pre.right
if not pre.right:
print(curr.val) # 前序遍历在此处理
pre.right = curr
curr = curr.left
else:
pre.right = None
curr = curr.right
Morris遍历虽然节省空间,但会修改树结构(临时创建线索),适用于只读场景或允许临时修改的情况。
前序遍历的特点是先访问根节点,再递归遍历左子树和右子树。这种顺序特别适合需要先处理父节点再处理子节点的场景,如树的复制、表达式树求值等。
python复制# 递归实现
def preorder(root):
if not root:
return []
return [root.val] + preorder(root.left) + preorder(root.right)
前序遍历的一个典型应用是序列化二叉树。以下是将二叉树序列化为字符串的示例:
python复制def serialize(root):
if not root:
return "None,"
return str(root.val) + "," + serialize(root.left) + serialize(root.right)
中序遍历的特点是先递归遍历左子树,再访问根节点,最后遍历右子树。对二叉搜索树(BST)进行中序遍历,会得到一个升序序列,这是BST的重要性质。
python复制# 递归实现
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
中序遍历的一个经典应用是验证二叉搜索树:
python复制def isValidBST(root):
prev = float('-inf')
stack = []
while stack or root:
while root:
stack.append(root)
root = root.left
root = stack.pop()
if root.val <= prev:
return False
prev = root.val
root = root.right
return True
后序遍历的特点是先递归遍历左右子树,最后访问根节点。这种顺序适用于需要先处理子节点再处理父节点的场景,如计算子树大小、释放树内存等。
python复制# 递归实现
def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]
后序遍历的一个典型应用是计算二叉树的高度:
python复制def treeHeight(root):
if not root:
return 0
left_height = treeHeight(root.left)
right_height = treeHeight(root.right)
return max(left_height, right_height) + 1
路径总和问题是DFS的经典应用场景,要求判断二叉树中是否存在从根到叶子的路径,其节点值之和等于给定目标。
python复制def hasPathSum(root, target):
if not root:
return False
if not root.left and not root.right: # 叶子节点
return root.val == target
remaining = target - root.val
return hasPathSum(root.left, remaining) or hasPathSum(root.right, remaining)
进阶问题:找出所有满足条件的路径。这时需要维护当前路径状态:
python复制def pathSum(root, target):
result = []
def dfs(node, path, remaining):
if not node:
return
path.append(node.val)
if not node.left and not node.right and remaining == node.val:
result.append(list(path))
dfs(node.left, path, remaining - node.val)
dfs(node.right, path, remaining - node.val)
path.pop() # 回溯,移除当前节点
dfs(root, [], target)
return result
寻找二叉树中两个节点的最近公共祖先,DFS也能优雅解决:
python复制def lowestCommonAncestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left and right: # p和q分布在两侧
return root
return left if left else right # 返回非空的一侧
二叉树的直径是指任意两个节点间最长路径的长度。通过DFS可以在O(n)时间内解决:
python复制def diameterOfBinaryTree(root):
diameter = 0
def depth(node):
nonlocal diameter
if not node:
return 0
left = depth(node.left)
right = depth(node.right)
diameter = max(diameter, left + right)
return max(left, right) + 1
depth(root)
return diameter
在某些问题中,我们可以通过特定条件提前终止DFS的某些分支,大幅提高效率。以路径总和问题为例:
python复制def hasPathSum(root, target):
if not root:
return False
stack = [(root, target - root.val)]
while stack:
node, remaining = stack.pop()
if not node.left and not node.right and remaining == 0:
return True
if node.right:
stack.append((node.right, remaining - node.right.val))
if node.left:
stack.append((node.left, remaining - node.left.val))
return False
对于存在重叠子问题的情况,可以使用记忆化技术存储中间结果。例如计算二叉树中所有左叶子之和:
python复制def sumOfLeftLeaves(root):
memo = {}
def dfs(node, is_left):
if not node:
return 0
if node in memo:
return memo[node]
if not node.left and not node.right and is_left:
memo[node] = node.val
return node.val
left_sum = dfs(node.left, True)
right_sum = dfs(node.right, False)
memo[node] = left_sum + right_sum
return memo[node]
return dfs(root, False)
忘记处理空节点:DFS递归时,必须首先检查节点是否为null,这是递归的终止条件。
修改遍历顺序:不同遍历顺序(前序/中序/后序)会导致完全不同的结果,必须根据问题需求选择正确的顺序。
忽略回溯操作:在需要维护路径状态的问题中,忘记在递归返回前"撤销选择"会导致错误结果。
重复访问节点:在图结构的DFS中(二叉树是特殊的有向无环图),必须标记已访问节点避免无限循环。
栈溢出风险:对于深度很大的树,递归实现可能导致栈溢出,应考虑改用迭代实现或增加递归深度限制。
许多树形DP问题本质上是DFS的扩展应用。例如计算二叉树中最大路径和:
python复制def maxPathSum(root):
max_sum = float('-inf')
def dfs(node):
nonlocal max_sum
if not node:
return 0
left = max(dfs(node.left), 0) # 舍弃负贡献
right = max(dfs(node.right), 0)
max_sum = max(max_sum, node.val + left + right)
return node.val + max(left, right)
dfs(root)
return max_sum
回溯算法本质上是带有状态重置的DFS。例如输出二叉树所有根到叶子的路径:
python复制def binaryTreePaths(root):
paths = []
def dfs(node, path):
if not node:
return
path.append(str(node.val))
if not node.left and not node.right:
paths.append("->".join(path))
dfs(node.left, path)
dfs(node.right, path)
path.pop() # 回溯
dfs(root, [])
return paths
许多分治算法在二叉树上的应用都是DFS的变体。例如构造二叉树从中序和后序遍历序列:
python复制def buildTree(inorder, postorder):
if not inorder:
return None
root_val = postorder[-1]
root = TreeNode(root_val)
idx = inorder.index(root_val)
root.left = buildTree(inorder[:idx], postorder[:idx])
root.right = buildTree(inorder[idx+1:], postorder[idx:-1])
return root
DFS的时间复杂度通常是O(n),其中n是树中节点数量,因为每个节点恰好被访问一次。但对于某些问题,如查找路径总和,最坏情况下可能需要访问所有路径,时间复杂度可能达到O(n^2)。
尾递归优化:某些语言(如Scheme)支持尾递归优化,可以避免栈溢出。但在Python等不支持尾递归优化的语言中效果有限。
迭代替代递归:对于深度不确定的大树,优先考虑迭代实现。
并行DFS:对于非常大的树,可以考虑将子树分配给不同线程/进程并行处理。
双向DFS:在某些特殊问题中,可以同时从根和叶子开始搜索,在中间相遇。
python复制def maxDepth(root):
if not root:
return 0
return 1 + max(maxDepth(root.left), maxDepth(root.right))
这是DFS最简单的应用之一,后序遍历的典型例子。时间复杂度O(n),空间复杂度O(h)。
python复制def isSymmetric(root):
def dfs(left, right):
if not left and not right:
return True
if not left or not right:
return False
return (left.val == right.val and
dfs(left.left, right.right) and
dfs(left.right, right.left))
return dfs(root.left, root.right) if root else True
这个问题展示了如何同时DFS两棵子树进行比较。
python复制def rightSideView(root):
view = []
def dfs(node, depth):
if not node:
return
if depth == len(view):
view.append(node.val)
dfs(node.right, depth + 1)
dfs(node.left, depth + 1)
dfs(root, 0)
return view
这个解法展示了如何通过控制遍历顺序(先右后左)来获取特定视角的视图。
虽然我们主要讨论了二叉树的DFS,但同样的原理可以推广到更一般的树结构:
对于N叉树的前序遍历示例:
python复制class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children if children else []
def nary_preorder(root):
if not root:
return []
result = [root.val]
for child in root.children:
result += nary_preorder(child)
return result
在实际工程项目中应用DFS时,还需要考虑以下实际问题:
栈深度限制:Python默认递归深度限制为1000,可通过sys.setrecursionlimit()调整,但需谨慎。
线程安全:多线程环境下,递归实现的DFS可能需要加锁或改用线程本地存储。
序列化/反序列化:DFS常用于树的序列化,但要考虑数据一致性和版本兼容性问题。
调试技巧:
测试边界条件:
理解DFS运行过程的一个好方法是使用可视化工具。推荐以下几种方式:
手工绘制遍历过程:在纸上画出二叉树,用不同颜色标记访问顺序。
使用在线可视化工具:
添加调试输出:
python复制def dfs(node, depth=0):
if not node:
return
print(" "*depth + f"Visiting {node.val}")
dfs(node.left, depth+1)
dfs(node.right, depth+1)
当面对一个新的二叉树问题时,可以按照以下步骤设计DFS解决方案:
以"求二叉树中所有左叶子之和"为例:
python复制def sumOfLeftLeaves(root):
total = 0
def dfs(node, is_left):
nonlocal total
if not node:
return
if not node.left and not node.right and is_left:
total += node.val
dfs(node.left, True)
dfs(node.right, False)
dfs(root, False)
return total
这个实现展示了如何通过额外参数is_left来标识左子节点,并在叶子节点处累加符合条件的值。