力扣144.二叉树前序遍历-递归和迭代

📖 前言

二叉树的前序遍历是树数据结构中最基础的遍历方式之一,也是面试中最常考的题目。本文将从零开始,全面解析二叉树前序遍历的各种实现方法,并分享面试中的实战技巧。

🎯 题目描述

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

解释要点

  1. 前序遍历的顺序是"根左右"

  2. 使用DFS深度优先搜索

  3. 递归的基线条件是遇到空节点

  4. 时间复杂度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

解释要点

  1. 用栈模拟递归过程

  2. 前序遍历需要先访问根节点,所以弹出节点后立即访问

  3. 入栈顺序先右后左,保证左子树先被处理

面试场景三:进阶问题

Q1:"如果树有数百万个节点,哪种方法更好?"

  • A:使用迭代法,避免递归栈溢出风险。

Q2:"前序遍历有哪些实际应用?"

  • A

    1. 复制二叉树结构

    2. 序列化二叉树

    3. 表达式树求值(前缀表达式)

    4. 目录树遍历

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)), "满二叉树"),
]

📝 常见错误与避坑指南

  1. 忘记处理空树if not root: return []

  2. 递归没有基线条件:导致无限递归

  3. 混淆遍历顺序:前序是"根左右",不是"左右根"

  4. 迭代法中入栈顺序错误:前序遍历需要先右后左入栈

  5. 使用原错误代码中的重复递归:确保每个节点只访问一次

🔗 相关题目

  1. 94. 二叉树的中序遍历

  2. 145. 二叉树的后序遍历

  3. 102. 二叉树的层序遍历

  4. 589. N叉树的前序遍历

🎓 总结

二叉树前序遍历的核心在于理解"根左右"的访问顺序。掌握以下几点:

  1. 递归法是基础,理解分治思想

  2. 迭代法是进阶,理解栈的应用

  3. 统一迭代法可扩展到中序、后序

  4. Morris遍历是空间优化的极致

面试黄金法则

  • 首选递归辅助函数法(清晰易懂)

  • 备选迭代栈法(应对禁用递归)

  • 主动分析复杂度(展现思考深度)

  • 考虑边界情况(体现代码健壮性)

记住,二叉树遍历是许多复杂算法的基础,扎实掌握前序遍历,将为学习更复杂的树算法打下坚实基础。


保持练习,持续进步! 二叉树的问题往往通过多练习就能形成肌肉记忆,建议每天至少练习一道二叉树相关题目。

相关推荐
好易学·数据结构2 小时前
可视化图解算法73:跳台阶(爬楼梯)
数据结构·算法·leetcode·动态规划·笔试
Salt_07282 小时前
DAY32 类的定义和方法
开发语言·python·算法·机器学习
Tisfy2 小时前
LeetCode 3433.统计用户被提及情况:(大)模拟
linux·算法·leetcode
一招定胜负3 小时前
逻辑回归核心原理与实践指南
算法·逻辑回归·线性回归
长安er3 小时前
LeetCode 98. 验证二叉搜索树 解题总结
java·数据结构·算法·leetcode·二叉树·力扣
薛不痒3 小时前
机器学习算法之线性回归&逻辑回归
算法·机器学习·逻辑回归
sin_hielo3 小时前
leetcode 3433
数据结构·算法·leetcode
Swift社区3 小时前
LeetCode 448 - 找到所有数组中消失的数字
算法·leetcode·职场和发展
OKkankan3 小时前
二叉搜索树
c语言·数据结构·c++·算法