📖 前言
二叉树的前序遍历是树数据结构中最基础的遍历方式之一,也是面试中最常考的题目。本文将从零开始,全面解析二叉树前序遍历的各种实现方法,并分享面试中的实战技巧。
🎯 题目描述
LeetCode 144. 二叉树的前序遍历
给定一个二叉树的根节点 root,返回其节点值的前序遍历结果。
示例 1:
text
输入:root = [1,null,2,3]
输出:[1,2,3]
🔍 什么是前序遍历?
前序遍历(Preorder Traversal)按照 根节点 → 左子树 → 右子树 的顺序访问二叉树的每个节点。
记忆口诀:"根左右"
遍历顺序图示:
text
1
/ \
2 3
/ \ \
4 5 6
前序遍历结果:[1, 2, 4, 5, 3, 6]
访问顺序:1 → 2 → 4 → 5 → 3 → 6
✅ 正确解法
解法一:递归辅助函数(面试首选⭐)
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
def dfs(node, result):
"""递归辅助函数"""
if not node:
return
result.append(node.val) # 1. 访问根节点
dfs(node.left, result) # 2. 遍历左子树
dfs(node.right, result) # 3. 遍历右子树
res = []
dfs(root, res)
return res
复杂度分析:
-
时间复杂度:O(n),每个节点访问一次
-
空间复杂度:O(h),递归栈深度,h为树的高度
解法二:简洁递归返回
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
# 基线条件:空树返回空列表
if not root:
return []
# 递归情况:根 + 左子树 + 右子树
return [root.val] + self.preorderTraversal(root.left) + self.preorderTraversal(root.right)
优点:
-
代码极其简洁(核心只有一行)
-
完美体现分治思想
-
易于理解记忆
缺点:
-
每次递归都创建新列表,空间效率较低
-
Python中列表拼接(
+)效率不如append()
选择递归方式
↓
是否需要传递额外参数或状态?
├── 是 → 使用辅助递归函数
│ ├── 需要收集多个结果? → 辅助函数
│ ├── 需要回溯? → 辅助函数
│ ├── 需要访问外部数据结构? → 辅助函数
│ └── 参数超过2个? → 辅助函数
│
└── 否 → 是否只是简单计算单个值?
├── 是 → 直接递归
└── 否 → 是否需要修改原树结构?
├── 是 → 直接递归(返回节点)
└── 否 → 辅助递归函数
解法三:迭代法(显式栈)
当面试官要求"不用递归"时,这是最佳选择。
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root] # 初始化栈,放入根节点
result = [] # 存储遍历结果
while stack:
# 1. 弹出栈顶节点并访问
node = stack.pop()
result.append(node.val)
# 2. 先将右子节点入栈,再将左子节点入栈
# 这样左子节点会先出栈,符合前序遍历顺序
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
迭代过程示例(树:[1,2,3,4,5]):
text
初始状态:
stack = [1], result = []
第1次循环:
弹出 1,result = [1]
压入右子节点 3,压入左子节点 2
stack = [3, 2]
第2次循环:
弹出 2,result = [1, 2]
压入右子节点 5,压入左子节点 4
stack = [3, 5, 4]
第3次循环:
弹出 4,result = [1, 2, 4]
无子节点,stack = [3, 5]
第4次循环:
弹出 5,result = [1, 2, 4, 5]
无子节点,stack = [3]
第5次循环:
弹出 3,result = [1, 2, 4, 5, 3]
无子节点,stack = []
循环结束,返回 [1, 2, 4, 5, 3]
解法四:统一迭代法(标记法)
这种方法可以统一处理前序、中序、后序遍历,适合需要掌握多种遍历的场景。
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [(root, False)] # (节点, 是否已访问)
result = []
while stack:
node, visited = stack.pop()
if visited:
# 如果节点已标记为访问过,则加入结果
result.append(node.val)
else:
# 前序遍历:根 -> 左 -> 右
# 入栈顺序:右 -> 左 -> 根(因为栈是LIFO)
if node.right:
stack.append((node.right, False))
if node.left:
stack.append((node.left, False))
stack.append((node, True)) # 当前节点标记为已访问
return result
解法五:Morris遍历(O(1)空间复杂度)
这是一种"神级"算法,在不使用递归和栈的情况下实现遍历,但会临时修改树结构。
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
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:
# 第一次访问当前节点,建立线索并访问
result.append(curr.val)
pre.right = curr # 建立线索
curr = curr.left
else:
# 第二次访问当前节点,断开线索
pre.right = None
curr = curr.right
return result
📊 方法对比与选择指南
| 方法 | 时间复杂度 | 空间复杂度 | 代码复杂度 | 适用场景 | 面试推荐度 |
|---|---|---|---|---|---|
| 递归辅助函数 | O(n) | O(h) | ⭐⭐ | 一般情况 | ⭐⭐⭐⭐⭐ |
| 简洁递归 | O(n) | O(n) | ⭐ | 代码简洁要求高 | ⭐⭐⭐⭐ |
| 迭代栈 | O(n) | O(h) | ⭐⭐⭐ | 禁用递归/深度大 | ⭐⭐⭐⭐⭐ |
| 统一迭代 | O(n) | O(h) | ⭐⭐⭐⭐ | 需要统一模板 | ⭐⭐⭐ |
| Morris遍历 | O(n) | O(1) | ⭐⭐⭐⭐⭐ | 空间限制严格 | ⭐⭐ |
💼 面试实战指南
面试场景一:直接写代码
面试官:"请实现二叉树的前序遍历。"
推荐回答:
python
# 边写边解释
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
# 使用递归辅助函数,这是最清晰的方法
def dfs(node, res):
if not node: # 基线条件:空节点
return
res.append(node.val) # 前序:先访问根节点
dfs(node.left, res) # 再递归遍历左子树
dfs(node.right, res) # 最后递归遍历右子树
result = []
dfs(root, result)
return result
解释要点:
-
前序遍历的顺序是"根左右"
-
使用DFS深度优先搜索
-
递归的基线条件是遇到空节点
-
时间复杂度O(n),空间复杂度O(h)
面试场景二:被要求不用递归
面试官:"能不能不用递归实现?"
推荐回答:
python
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
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
解释要点:
-
用栈模拟递归过程
-
前序遍历需要先访问根节点,所以弹出节点后立即访问
-
入栈顺序先右后左,保证左子树先被处理
面试场景三:进阶问题
Q1:"如果树有数百万个节点,哪种方法更好?"
- A:使用迭代法,避免递归栈溢出风险。
Q2:"前序遍历有哪些实际应用?"
-
A:
-
复制二叉树结构
-
序列化二叉树
-
表达式树求值(前缀表达式)
-
目录树遍历
-
Q3:"如何修改代码实现中序/后序遍历?"
-
A:
-
中序:
左 → 根 → 右→ 调整访问顺序 -
后序:
左 → 右 → 根→ 调整访问顺序
-
python
# 中序遍历(递归)
def inorder(node, res):
if not node: return
inorder(node.left, res) # 左
res.append(node.val) # 根
inorder(node.right, res) # 右
# 后序遍历(递归)
def postorder(node, res):
if not node: return
postorder(node.left, res) # 左
postorder(node.right, res) # 右
res.append(node.val) # 根
🧪 测试用例设计
优秀的面试者会主动考虑各种边界情况:
python
# 测试用例集
test_cases = [
# (输入, 期望输出, 说明)
(None, [], "空树"),
([1], [1], "单节点"),
([1,2,3], [1,2,3], "完全二叉树"),
([1,None,2,3], [1,2,3], "右斜树"),
([1,2,None,3,4], [1,2,3,4], "左子树复杂"),
([i for i in range(1, 16)], list(range(1, 16)), "满二叉树"),
]
📝 常见错误与避坑指南
-
忘记处理空树 :
if not root: return [] -
递归没有基线条件:导致无限递归
-
混淆遍历顺序:前序是"根左右",不是"左右根"
-
迭代法中入栈顺序错误:前序遍历需要先右后左入栈
-
使用原错误代码中的重复递归:确保每个节点只访问一次
🔗 相关题目
🎓 总结
二叉树前序遍历的核心在于理解"根左右"的访问顺序。掌握以下几点:
-
递归法是基础,理解分治思想
-
迭代法是进阶,理解栈的应用
-
统一迭代法可扩展到中序、后序
-
Morris遍历是空间优化的极致
面试黄金法则:
-
首选递归辅助函数法(清晰易懂)
-
备选迭代栈法(应对禁用递归)
-
主动分析复杂度(展现思考深度)
-
考虑边界情况(体现代码健壮性)
记住,二叉树遍历是许多复杂算法的基础,扎实掌握前序遍历,将为学习更复杂的树算法打下坚实基础。
保持练习,持续进步! 二叉树的问题往往通过多练习就能形成肌肉记忆,建议每天至少练习一道二叉树相关题目。