1. 二叉树遍历基础与LeetCode题目解析
在算法与数据结构的学习中,二叉树遍历是最基础也是最重要的内容之一。LeetCode上关于二叉树遍历的题目往往被用作面试的"热身题",但真正掌握其原理并能灵活应用却并不简单。今天我们将深入探讨四种经典的二叉树遍历方式:前序遍历、中序遍历、后序遍历以及层序遍历。
二叉树是一种非线性数据结构,由节点和边组成,每个节点最多有两个子节点,分别称为左子节点和右子节点。遍历二叉树意味着按照某种顺序访问树中的所有节点。不同的遍历顺序会产生不同的节点访问序列,这在解决实际问题时有着不同的应用场景。
LeetCode 144、145、94这三道题目分别对应二叉树的前序、后序和中序遍历,而102题则考察层序遍历。这些题目看似简单,但要做到一次写对、理解透彻,需要我们对递归和迭代两种实现方式都有清晰的认识。更重要的是,理解为什么在某些特定场景下要选择某种遍历方式。
提示:虽然递归实现简洁易懂,但在面试中,面试官往往会要求同时给出迭代解法,因为递归解法可能会因为栈深度问题在大数据量时出现栈溢出。
2. 前序遍历(LeetCode 144)的实现与解析
前序遍历的顺序是:根节点 → 左子树 → 右子树。这种遍历方式的特点是首先访问根节点,这种特性使得它在需要优先处理父节点再处理子节点的场景中非常有用,比如树的序列化、表达式的语法树求值等。
2.1 递归实现前序遍历
递归实现是最直观的方式,完美匹配了遍历的定义:
python复制def preorderTraversal(root):
result = []
def traversal(node):
if not node:
return
result.append(node.val) # 访问根节点
traversal(node.left) # 遍历左子树
traversal(node.right) # 遍历右子树
traversal(root)
return result
这种实现方式简洁明了,时间复杂度为O(n),空间复杂度在最坏情况下(树退化为链表)也是O(n)。
2.2 迭代实现前序遍历
迭代实现需要显式使用栈来模拟递归的调用过程:
python复制def preorderTraversal(root):
if not root:
return []
stack, result = [root], []
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
迭代实现的关键在于理解栈的"后进先出"特性。我们先将右子节点压栈,再将左子节点压栈,这样左子节点会先被弹出处理,保证了"根→左→右"的顺序。
注意:迭代实现中,节点访问顺序与入栈顺序相反,这是很多初学者容易混淆的地方。建议通过画图来理解这个过程。
3. 中序遍历(LeetCode 94)的实现与应用
中序遍历的顺序是:左子树 → 根节点 → 右子树。这种遍历方式的特点是对于二叉搜索树(BST),中序遍历会得到一个升序序列,这使得它在BST相关的问题中特别有用,比如验证BST、BST中第k小的元素等。
3.1 递归实现中序遍历
python复制def inorderTraversal(root):
result = []
def traversal(node):
if not node:
return
traversal(node.left) # 遍历左子树
result.append(node.val) # 访问根节点
traversal(node.right) # 遍历右子树
traversal(root)
return result
3.2 迭代实现中序遍历
中序遍历的迭代实现比前序要复杂一些,因为访问节点的顺序和处理节点的顺序不一致:
python复制def inorderTraversal(root):
result, stack = [], []
curr = root
while curr or stack:
# 先将左子节点全部入栈
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
result.append(curr.val)
curr = curr.right
return result
这种实现方式的关键在于理解"左链入栈"的过程。我们不断地将当前节点的左子节点压入栈中,直到没有左子节点为止,然后弹出栈顶节点访问,再转向其右子节点。
在实际应用中,中序遍历的一个典型场景是BST的验证。例如,判断一棵树是否是BST,可以通过中序遍历检查结果是否有序来实现。
4. 后序遍历(LeetCode 145)的特点与实现
后序遍历的顺序是:左子树 → 右子树 → 根节点。这种遍历方式的特点是最后访问根节点,这使得它在需要先处理子节点再处理父节点的场景中非常有用,比如计算子树的大小、释放树的内存等。
4.1 递归实现后序遍历
python复制def postorderTraversal(root):
result = []
def traversal(node):
if not node:
return
traversal(node.left) # 遍历左子树
traversal(node.right) # 遍历右子树
result.append(node.val) # 访问根节点
traversal(root)
return result
4.2 迭代实现后序遍历
后序遍历的迭代实现最为复杂,有几种不同的思路。其中一种巧妙的方法是修改前序遍历的顺序,然后反转结果:
python复制def postorderTraversal(root):
if not root:
return []
stack, result = [root], []
while stack:
node = stack.pop()
result.append(node.val)
# 左子节点先入栈,保证右子节点先出栈
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return result[::-1] # 反转结果
这种方法实际上是按照"根→右→左"的顺序遍历,然后反转结果得到"左→右→根"的后序遍历顺序。
另一种更符合后序遍历本质的迭代实现需要使用指针来记录上一个访问的节点:
python复制def postorderTraversal(root):
result, stack = [], []
prev = None # 记录上一个访问的节点
curr = root
while curr or stack:
# 左子节点全部入栈
while curr:
stack.append(curr)
curr = curr.left
curr = stack[-1]
# 如果右子节点不存在或已经访问过
if not curr.right or curr.right == prev:
result.append(curr.val)
stack.pop()
prev = curr
curr = None
else:
curr = curr.right
return result
这种实现方式虽然复杂,但更直接地反映了后序遍历的过程,有助于深入理解遍历的本质。
5. 层序遍历(LeetCode 102)的实现与变种
层序遍历是按照树的层次,从上到下、从左到右依次访问每个节点。这种遍历方式在需要按层次处理节点的问题中非常有用,比如求树的深度、找树的最大宽度等。
5.1 基本层序遍历实现
层序遍历通常使用队列来实现:
python复制def levelOrder(root):
if not root:
return []
queue, result = [root], []
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.pop(0)
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
这种实现使用队列来保证节点的访问顺序。每次处理一层的所有节点,并将它们的子节点加入队列,为下一层的遍历做准备。
5.2 层序遍历的常见变种
层序遍历有很多变种问题,例如:
- 锯齿形层序遍历(Zigzag Level Order):奇数层从左到右,偶数层从右到左
- 每层的最大值
- 每层的平均值
- 右视图(只返回每层最右边的节点)
- 左视图(只返回每层最左边的节点)
以锯齿形层序遍历为例:
python复制def zigzagLevelOrder(root):
if not root:
return []
queue, result = [root], []
left_to_right = True
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.pop(0)
if left_to_right:
current_level.append(node.val)
else:
current_level.insert(0, node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
left_to_right = not left_to_right
return result
层序遍历在解决实际问题时非常有用,比如在社交网络中计算人与人之间的"距离",或者在游戏中实现基于网格的路径查找算法。
6. 遍历方式的选择与实际应用
不同的遍历方式适用于不同的场景,理解它们的特点对于解决实际问题至关重要。
6.1 遍历方式的选择指南
-
前序遍历:当需要先处理父节点再处理子节点时使用
- 树的序列化
- 复制树结构
- 表达式树的前缀表示
-
中序遍历:对于二叉搜索树,可以得到有序序列
- 验证二叉搜索树
- 二叉搜索树中第k小的元素
- 将二叉搜索树转换为有序链表
-
后序遍历:当需要先处理子节点再处理父节点时使用
- 计算子树的大小
- 释放树的内存
- 表达式树的后缀表示
-
层序遍历:当需要按层次处理节点时使用
- 找树的最大宽度
- 找树的最小深度
- 判断是否为完全二叉树
6.2 实际应用案例分析
案例1:表达式求值
表达式可以用表达式树表示,其中叶子节点是操作数,内部节点是操作符。不同的遍历方式对应不同的表达式表示:
- 前序遍历 → 前缀表达式(波兰表示法)
- 中序遍历 → 中缀表达式(需要括号)
- 后序遍历 → 后缀表达式(逆波兰表示法)
案例2:树的序列化与反序列化
前序遍历非常适合用于树的序列化,因为序列化的第一个元素就是根节点,这使得反序列化过程非常直观:
python复制def serialize(root):
if not root:
return 'None'
return ','.join([str(root.val), serialize(root.left), serialize(root.right)])
def deserialize(data):
def helper(queue):
val = queue.pop(0)
if val == 'None':
return None
node = TreeNode(int(val))
node.left = helper(queue)
node.right = helper(queue)
return node
return helper(data.split(','))
案例3:验证二叉搜索树
利用中序遍历的性质,我们可以高效地验证一棵树是否是BST:
python复制def isValidBST(root):
stack, prev = [], float('-inf')
curr = root
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
if curr.val <= prev:
return False
prev = curr.val
curr = curr.right
return True
7. 常见错误与优化技巧
在实现二叉树遍历时,即使是经验丰富的开发者也会犯一些常见错误。了解这些陷阱可以帮助我们写出更健壮的代码。
7.1 常见错误分析
-
递归实现中的全局变量误用:
python复制# 错误示例 result = [] def inorderTraversal(root): if not root: return inorderTraversal(root.left) result.append(root.val) inorderTraversal(root.right) return result这种写法在多次调用函数时会导致结果累积。正确的做法是将result定义在函数内部或作为参数传递。
-
迭代实现中的指针丢失:
python复制# 错误示例 def inorderTraversal(root): stack, result = [], [] curr = root while curr or stack: if curr: stack.append(curr) else: curr = stack.pop() result.append(curr.val) curr = curr.right # 这里可能丢失左子树 return result这个实现会跳过某些左子树的遍历。
-
层序遍历中的层次混淆:
python复制# 错误示例 def levelOrder(root): queue, result = [root], [] while queue: node = queue.pop(0) result.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) return result这个实现没有区分不同层次的节点,将所有节点混在一起。
7.2 性能优化技巧
-
迭代实现的统一写法:
前序、中序和后序遍历的迭代实现可以有统一的模板,只需调整节点访问的顺序:python复制def traversal(root): result, stack = [], [] if root: stack.append(root) while stack: node = stack.pop() if node: # 调整下面三行的顺序可实现不同遍历 if node.right: stack.append(node.right) # 右 if node.left: stack.append(node.left) # 左 stack.append(node) # 中 stack.append(None) # 标记待处理节点 else: node = stack.pop() result.append(node.val) return result -
Morris遍历:
Morris遍历是一种空间复杂度为O(1)的遍历算法,它通过修改树的结构(临时)来实现遍历,最后再恢复树的结构:python复制def inorderTraversal(root): result = [] curr = root while curr: if not curr.left: result.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 # 删除线索 result.append(curr.val) curr = curr.right return result -
双向队列优化层序遍历:
使用collections.deque可以优化层序遍历的性能,因为list的pop(0)操作是O(n)复杂度:python复制from collections import deque def levelOrder(root): if not root: return [] queue, result = deque([root]), [] 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
在实际编程中,理解这些遍历算法的本质比记忆代码更重要。建议通过画图来理解每种遍历方式的节点访问顺序,以及递归和迭代实现的内在联系。
