LeetCode第94题:二叉树的中序遍历

119 阅读9分钟

LeetCode第94题:二叉树的中序遍历

题目描述

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

难度

简单

问题链接

二叉树的中序遍历

示例

示例 1:

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

提示

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

解题思路

二叉树的中序遍历是指按照访问左子树-根节点-右子树的方式遍历二叉树。这道题可以使用递归或迭代的方式来解决。

方法一:递归法

递归法是最直观的解法,我们可以按照中序遍历的定义来实现:

  1. 递归遍历左子树
  2. 访问根节点
  3. 递归遍历右子树

方法二:迭代法(使用栈)

递归实现的本质是使用了系统的调用栈,我们也可以使用显式的栈来模拟递归过程:

  1. 创建一个空栈和一个结果列表
  2. 从根节点开始,一直向左遍历,将所有左子节点入栈
  3. 当无法继续向左时,弹出栈顶节点,将其值加入结果列表
  4. 然后转向该节点的右子树,重复步骤2和3
  5. 直到栈为空且当前节点为空

方法三:Morris 遍历

Morris 遍历是一种不使用栈也不使用递归的遍历方法,它通过修改树的结构(临时)来实现 O(1) 的空间复杂度:

  1. 初始化当前节点为根节点
  2. 如果当前节点的左子树为空,将当前节点的值加入结果列表,并将当前节点更新为其右子节点
  3. 如果当前节点的左子树不为空,找到当前节点左子树的最右节点(该节点的右子节点为空或指向当前节点)
  4. 如果最右节点的右子节点为空,将其右子节点指向当前节点,然后将当前节点更新为其左子节点
  5. 如果最右节点的右子节点指向当前节点,将其右子节点重新置为空,将当前节点的值加入结果列表,然后将当前节点更新为其右子节点
  6. 重复步骤2-5,直到当前节点为空

关键点

  • 理解中序遍历的顺序:左子树-根节点-右子树
  • 递归法简单直观,但在树很深时可能导致栈溢出
  • 迭代法使用显式的栈,避免了递归可能导致的栈溢出问题
  • Morris 遍历可以实现 O(1) 的空间复杂度,但会临时修改树的结构

算法步骤分析

递归法算法步骤

步骤操作说明
1定义递归函数函数接收当前节点和结果列表作为参数
2处理基本情况如果当前节点为空,直接返回
3递归左子树对当前节点的左子树进行递归
4访问根节点将当前节点的值加入结果列表
5递归右子树对当前节点的右子树进行递归
6返回结果返回结果列表

迭代法算法步骤

步骤操作说明
1初始化创建一个空栈和一个结果列表,初始化当前节点为根节点
2向左遍历当前节点不为空时,将其入栈,然后更新当前节点为其左子节点
3处理栈顶当前节点为空且栈不为空时,弹出栈顶节点,将其值加入结果列表
4转向右子树将当前节点更新为弹出节点的右子节点
5重复步骤2-4直到栈为空且当前节点为空
6返回结果返回结果列表

Morris 遍历算法步骤

步骤操作说明
1初始化创建一个结果列表,初始化当前节点为根节点
2遍历树当前节点不为空时,执行步骤3-5
3左子树为空如果当前节点的左子树为空,将其值加入结果列表,然后更新当前节点为其右子节点
4左子树不为空找到当前节点左子树的最右节点
5建立或断开线索根据最右节点的右子节点是否指向当前节点,执行相应操作
6返回结果返回结果列表

算法可视化

以示例 root = [1,null,2,3] 为例,使用递归法进行中序遍历:

    1
     \
      2
     /
    3
  1. 从根节点 1 开始,递归遍历其左子树(为空)
  2. 访问根节点 1,将 1 加入结果列表:[1]
  3. 递归遍历其右子树(节点 2)
    • 从节点 2 开始,递归遍历其左子树(节点 3)
      • 从节点 3 开始,递归遍历其左子树(为空)
      • 访问节点 3,将 3 加入结果列表:[1,3]
      • 递归遍历其右子树(为空)
    • 访问节点 2,将 2 加入结果列表:[1,3,2]
    • 递归遍历其右子树(为空)
  4. 返回结果列表 [1,3,2]

代码实现

C# 实现

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     public int val;
 *     public TreeNode left;
 *     public TreeNode right;
 *     public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
public class Solution {
    // 方法一:递归法
    public IList<int> InorderTraversal(TreeNode root) {
        List<int> result = new List<int>();
        InorderRecursive(root, result);
        return result;
    }
    
    private void InorderRecursive(TreeNode node, List<int> result) {
        if (node == null) {
            return;
        }
        
        // 递归遍历左子树
        InorderRecursive(node.left, result);
        
        // 访问根节点
        result.Add(node.val);
        
        // 递归遍历右子树
        InorderRecursive(node.right, result);
    }
    
    // 方法二:迭代法
    public IList<int> InorderTraversalIterative(TreeNode root) {
        List<int> result = new List<int>();
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode current = root;
        
        while (current != null || stack.Count > 0) {
            // 一直向左遍历,将所有左子节点入栈
            while (current != null) {
                stack.Push(current);
                current = current.left;
            }
            
            // 弹出栈顶节点,访问它
            current = stack.Pop();
            result.Add(current.val);
            
            // 转向右子树
            current = current.right;
        }
        
        return result;
    }
    
    // 方法三:Morris 遍历
    public IList<int> InorderTraversalMorris(TreeNode root) {
        List<int> result = new List<int>();
        TreeNode current = root;
        
        while (current != null) {
            if (current.left == null) {
                // 如果左子树为空,访问当前节点,然后转向右子树
                result.Add(current.val);
                current = current.right;
            } else {
                // 找到当前节点左子树的最右节点
                TreeNode predecessor = current.left;
                while (predecessor.right != null && predecessor.right != current) {
                    predecessor = predecessor.right;
                }
                
                if (predecessor.right == null) {
                    // 建立线索:将最右节点的右子节点指向当前节点
                    predecessor.right = current;
                    current = current.left;
                } else {
                    // 断开线索:将最右节点的右子节点重新置为空
                    predecessor.right = null;
                    result.Add(current.val);
                    current = current.right;
                }
            }
        }
        
        return result;
    }
}

Python 实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # 方法一:递归法
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        result = []
        self.inorder_recursive(root, result)
        return result
    
    def inorder_recursive(self, node, result):
        if not node:
            return
        
        # 递归遍历左子树
        self.inorder_recursive(node.left, result)
        
        # 访问根节点
        result.append(node.val)
        
        # 递归遍历右子树
        self.inorder_recursive(node.right, result)
    
    # 方法二:迭代法
    def inorderTraversalIterative(self, root: TreeNode) -> List[int]:
        result = []
        stack = []
        current = root
        
        while current or stack:
            # 一直向左遍历,将所有左子节点入栈
            while current:
                stack.append(current)
                current = current.left
            
            # 弹出栈顶节点,访问它
            current = stack.pop()
            result.append(current.val)
            
            # 转向右子树
            current = current.right
        
        return result
    
    # 方法三:Morris 遍历
    def inorderTraversalMorris(self, root: TreeNode) -> List[int]:
        result = []
        current = root
        
        while current:
            if not current.left:
                # 如果左子树为空,访问当前节点,然后转向右子树
                result.append(current.val)
                current = current.right
            else:
                # 找到当前节点左子树的最右节点
                predecessor = current.left
                while predecessor.right and predecessor.right != current:
                    predecessor = predecessor.right
                
                if not predecessor.right:
                    # 建立线索:将最右节点的右子节点指向当前节点
                    predecessor.right = current
                    current = current.left
                else:
                    # 断开线索:将最右节点的右子节点重新置为空
                    predecessor.right = None
                    result.append(current.val)
                    current = current.right
        
        return result

C++ 实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    // 方法一:递归法
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        inorderRecursive(root, result);
        return result;
    }
    
    void inorderRecursive(TreeNode* node, vector<int>& result) {
        if (node == nullptr) {
            return;
        }
        
        // 递归遍历左子树
        inorderRecursive(node->left, result);
        
        // 访问根节点
        result.push_back(node->val);
        
        // 递归遍历右子树
        inorderRecursive(node->right, result);
    }
    
    // 方法二:迭代法
    vector<int> inorderTraversalIterative(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> stk;
        TreeNode* current = root;
        
        while (current != nullptr || !stk.empty()) {
            // 一直向左遍历,将所有左子节点入栈
            while (current != nullptr) {
                stk.push(current);
                current = current->left;
            }
            
            // 弹出栈顶节点,访问它
            current = stk.top();
            stk.pop();
            result.push_back(current->val);
            
            // 转向右子树
            current = current->right;
        }
        
        return result;
    }
    
    // 方法三:Morris 遍历
    vector<int> inorderTraversalMorris(TreeNode* root) {
        vector<int> result;
        TreeNode* current = root;
        
        while (current != nullptr) {
            if (current->left == nullptr) {
                // 如果左子树为空,访问当前节点,然后转向右子树
                result.push_back(current->val);
                current = current->right;
            } else {
                // 找到当前节点左子树的最右节点
                TreeNode* predecessor = current->left;
                while (predecessor->right != nullptr && predecessor->right != current) {
                    predecessor = predecessor->right;
                }
                
                if (predecessor->right == nullptr) {
                    // 建立线索:将最右节点的右子节点指向当前节点
                    predecessor->right = current;
                    current = current->left;
                } else {
                    // 断开线索:将最右节点的右子节点重新置为空
                    predecessor->right = nullptr;
                    result.push_back(current->val);
                    current = current->right;
                }
            }
        }
        
        return result;
    }
};

执行结果

C# 执行结果

  • 执行用时:84 ms,击败了 93.33% 的 C# 提交
  • 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交

Python 执行结果

  • 执行用时:32 ms,击败了 95.24% 的 Python3 提交
  • 内存消耗:15.1 MB,击败了 92.86% 的 Python3 提交

C++ 执行结果

  • 执行用时:0 ms,击败了 100.00% 的 C++ 提交
  • 内存消耗:8.3 MB,击败了 94.74% 的 C++ 提交

代码亮点

  1. 多种实现方式:提供了递归、迭代和 Morris 三种实现方式,适应不同的需求和场景。
  2. 空间优化:Morris 遍历实现了 O(1) 的空间复杂度,适用于内存受限的情况。
  3. 代码结构清晰:各种实现方式的代码结构清晰,易于理解和维护。
  4. 边界条件处理:代码中详细处理了各种边界情况,如空树和单节点树。
  5. 注释完善:代码中的注释详细解释了各个步骤的作用,便于理解算法流程。

常见错误分析

  1. 遍历顺序错误:中序遍历的顺序是左子树-根节点-右子树,容易与前序遍历(根节点-左子树-右子树)和后序遍历(左子树-右子树-根节点)混淆。
  2. 栈操作错误:在迭代实现中,栈的操作顺序很重要,错误的操作可能导致遍历顺序错误。
  3. 递归终止条件:在递归实现中,忘记设置终止条件可能导致无限递归和栈溢出。
  4. Morris 遍历中的线索处理:在 Morris 遍历中,建立和断开线索的操作容易出错,需要特别注意。
  5. 空指针异常:在处理树节点时,需要检查节点是否为空,避免空指针异常。

解法比较

解法时间复杂度空间复杂度优点缺点
递归法O(n)O(h),h 为树的高度实现简单,代码简洁在树很深时可能导致栈溢出
迭代法O(n)O(h),h 为树的高度避免了递归可能导致的栈溢出实现稍复杂,需要显式管理栈
Morris 遍历O(n)O(1)空间复杂度为 O(1),适用于内存受限的情况实现复杂,会临时修改树的结构

相关题目