1. 二叉树遍历基础:理解三种遍历方式
作为一名算法工程师,我始终认为二叉树遍历是每个程序员必须掌握的"内功心法"。就像武术中的马步一样,看似简单却是一切高级技巧的基础。今天我们就来深入探讨LeetCode上经典的二叉树遍历三连题(144前序、94中序、145后序),我会结合自己刷题和面试的经验,分享一些教科书上不会写的实战技巧。
先明确三种遍历方式的定义:
- 前序遍历:根节点 → 左子树 → 右子树
- 中序遍历:左子树 → 根节点 → 右子树
- 后序遍历:左子树 → 右子树 → 根节点
记忆口诀:"前中后"指的是根节点的访问时机。就像参加一个会议:
- 前序:先自我介绍(根),再听左边人发言,最后听右边人发言
- 中序:先听左边人发言,再自我介绍,最后听右边人发言
- 后序:先听左右发言,最后才轮到自己总结
关键理解:遍历顺序的核心差异在于处理根节点的时机,而左子树永远优先于右子树这个规则是不变的。
2. 递归解法:优雅简洁的数学思维
递归写法是理解二叉树遍历最直观的方式。我们先来看前序遍历的Python实现:
python复制def preorderTraversal(root):
def dfs(node):
if not node: # 递归终止条件
return
res.append(node.val) # 处理根节点
dfs(node.left) # 递归左子树
dfs(node.right) # 递归右子树
res = []
dfs(root)
return res
中序和后序遍历只需调整三行代码的顺序:
python复制# 中序
res.append(node.val) # 移到左递归之后
# 后序
res.append(node.val) # 移到右递归之后
递归的底层原理:
- 系统维护一个隐式的调用栈(call stack)
- 每层递归都会将当前状态(变量、返回地址)压栈
- 遇到空节点时开始回溯(弹栈)
实战经验:递归深度过大可能导致栈溢出。Python默认递归深度约1000层,对于极端不平衡的二叉树(如链表状的树),建议改用迭代写法。
3. 迭代解法:手动模拟递归栈
面试时经常会被要求"不用递归实现遍历",这时就需要手动维护栈来模拟递归过程。不同遍历方式的迭代写法差异较大,我们分别来看。
3.1 前序迭代模板
前序遍历的迭代实现最为直观:
python复制def preorderTraversal(root):
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
# 右孩子先入栈(保证左孩子先处理)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return res
3.2 后序迭代技巧
后序遍历有个巧妙技巧:调整前序遍历的顺序(根→右→左),然后反转结果就是后序(左→右→根):
python复制def postorderTraversal(root):
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
# 这次左孩子先入栈
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res[::-1] # 关键反转步骤
3.3 中序迭代的挑战
中序遍历是三种中最复杂的,因为访问顺序和处理顺序不一致。需要用一个指针标记当前节点,配合栈来跟踪待处理节点:
python复制def inorderTraversal(root):
res = []
stack = []
curr = root
while curr or stack:
# 先一路向左走到底
while curr:
stack.append(curr)
curr = curr.left
# 弹出栈顶处理
curr = stack.pop()
res.append(curr.val)
# 转向右子树
curr = curr.right
return res
调试技巧:在纸上画出栈和指针的变化过程,这是理解迭代中序的关键。建议用这个例子练习:
code复制1 / \ 2 3 / \ 4 5
4. 统一迭代法:一招通吃三种遍历
为了统一三种遍历的写法,可以使用"标记法"——在访问过的节点后插入一个空节点作为标记。当遇到空节点时,才处理真正的节点:
python复制def inorderTraversal(root):
res = []
stack = [root]
while stack:
node = stack.pop()
if not node:
continue
if isinstance(node, TreeNode):
# 根据遍历类型调整入栈顺序
stack.append(node.right) # 右
stack.append(node.left) # 左
stack.append(node.val) # 根
else:
res.append(node)
return res
这种写法的优势是:
- 三种遍历只需调整入栈顺序
- 避免特殊条件判断
- 代码结构高度统一
缺点是引入了类型判断,可能影响可读性。适合在理解传统写法后作为拓展学习。
5. 常见问题与性能优化
5.1 递归改迭代的通用方法
- 显式维护一个栈替代系统调用栈
- 将递归函数的参数作为栈元素存储
- 使用循环替代递归调用
- 在适当位置插入处理逻辑
5.2 迭代写法的易错点
- 前序/后序混淆左右子节点入栈顺序
- 中序遍历忘记移动curr指针
- 栈空判断不完整导致无限循环
- 处理空节点时未跳过继续循环
5.3 时间复杂度分析
所有遍历方式都是:
- 时间复杂度:O(n) 每个节点访问一次
- 空间复杂度:O(h) h为树高,最坏情况O(n)
性能优化技巧:对于特别大的树,迭代写法通常比递归更节省内存,因为可以控制栈的大小。在Python中,递归深度限制可以通过sys.setrecursionlimit()调整,但不推荐在生产环境中使用。
6. 实际应用场景
理解这些遍历方式在实际工程中有广泛用途:
- 前序:复制二叉树结构(先创建节点再复制子树)
- 中序:BST得到有序序列
- 后序:计算目录大小(先知道子目录大小才能算总大小)
- 序列化/反序列化二叉树
- 表达式树求值
例如,计算二叉树高度的后序实现:
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
7. 扩展思考:Morris遍历
对于追求极致空间复杂度的场景,可以使用Morris遍历算法,达到O(1)空间复杂度(不含结果存储)。其核心思想是利用叶子节点的空指针临时存储信息:
python复制def inorderMorris(root):
res = []
curr = root
while curr:
if not curr.left:
res.append(curr.val)
curr = curr.right
else:
# 找前驱节点
pre = curr.left
while pre.right and pre.right != curr:
pre = pre.right
if not pre.right:
pre.right = curr # 建立临时链接
curr = curr.left
else:
pre.right = None # 恢复树结构
res.append(curr.val)
curr = curr.right
return res
这种算法虽然高效,但会临时修改树结构,适用于只读场景或明确允许修改的情况。