【LeetCode Hot100 刷题日记(36/100)】94. 二叉树的中序遍历——树、栈、深度优先搜索、二叉树、递归、Morris 遍历 🌲

65 阅读7分钟

📌 题目链接:94. 二叉树的中序遍历 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:树、栈、深度优先搜索、二叉树、递归、Morris 遍历

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(h),其中 h 是树的高度,最坏情况 O(n),最优情况 O(log n)


✅ 为什么这道题是“热题”?🤔

这道题虽然标记为“简单”,但却是二叉树遍历的基础核心题,在面试中出现频率极高,常作为考察候选人对 递归思维、栈结构理解、空间优化能力 的入口题。它不仅是后续很多树类问题(如验证BST、构造二叉树、路径和等)的前置知识,更是掌握 DFS(深度优先搜索)思想 的关键一步。

📌 面试官可能问你

  • “你能用三种方式实现中序遍历吗?”
  • “递归和迭代的区别是什么?各自的优劣?”
  • “有没有办法做到空间复杂度 O(1)?”
  • “如何判断一棵树是否是二叉搜索树?”

这些问题都建立在这道题的基础上!所以别小看它!


🧠 题目分析

给定一个二叉树的根节点 root,返回它的 中序遍历结果(左 → 根 → 右)。

📊 示例回顾:

示例1:
输入: root = [1,null,2,3]
       1
        \
         2
        /
       3
输出: [1,3,2]

示例2:
输入: root = []
输出: []

示例3:
输入: root = [1]
输出: [1]

✅ 中序遍历规则:左子树 → 根节点 → 右子树

这个顺序对于二叉搜索树(BST) 来说非常有用:它会按升序输出所有节点值。


🔍 核心算法及代码讲解

我们从三个角度来深入剖析本题的核心算法:

✅ 方法一:递归 —— 最直观,最自然的方式

🎯 思路:

  • 模拟“左 → 根 → 右”的访问顺序。
  • 使用函数调用栈自动维护遍历状态。
  • 是 DFS 的经典体现。

🧩 代码实现(带行注释):

// 递归实现中序遍历
void inorder(TreeNode* root, vector<int>& res) {
    if (!root) { // 基础情况:空节点直接返回
        return;
    }
    inorder(root->left, res);   // 先递归处理左子树
    res.push_back(root->val);   // 再访问当前节点
    inorder(root->right, res);  // 最后递归处理右子树
}

💡 递归的本质是隐式使用系统栈,每次函数调用都会压入栈帧,直到叶子节点,再逐层弹出执行。

⚙️ 复杂度分析:

  • 时间复杂度:O(n),每个节点访问一次。
  • 空间复杂度:O(h),h 是树的高度(递归栈深度),最坏情况退化成链表时为 O(n),平衡树为 O(log n)。

✅ 方法二:迭代 —— 显式使用栈模拟递归过程

🎯 思路:

  • 手动维护一个栈来代替系统栈。
  • 按照“先左到底,再弹出并访问,最后转向右”的逻辑进行。
  • 关键点在于:什么时候该访问当前节点?当它没有左孩子或者左子树已经遍历完时。

🧩 代码实现(带行注释):

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> res;
    stack<TreeNode*> stk;  // 显式栈用于模拟递归过程
    TreeNode* cur = root;  // 当前遍历的节点

    while (cur != nullptr || !stk.empty()) {
        // 一直向左走,将路径上的所有节点压入栈
        while (cur != nullptr) {
            stk.push(cur);
            cur = cur->left;
        }

        // 弹出栈顶节点(即最左边的未访问节点)
        cur = stk.top();
        stk.pop();
        res.push_back(cur->val);  // 访问该节点

        // 转向右子树继续遍历
        cur = cur->right;
    }
    return res;
}

🔁 迭代版本的核心思想是:“左走到头 → 弹出 → 访问 → 右转”。

⚙️ 复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(h),栈最大深度等于树高。

🛠️ 注意:迭代法比递归更可控,适合在不允许递归调用或栈溢出风险高的场景下使用。


✅ 方法三:Morris 中序遍历 —— 空间优化的巅峰艺术 🎨

🎯 思路:

  • 不使用额外栈,也不使用递归。
  • 利用树本身的结构,在遍历时临时修改指针关系,从而节省空间。
  • 核心思想:利用叶子节点的右指针“回溯”到父节点

🔁 Morris 遍历流程详解:

设当前节点为 x,分两种情况:

  1. 如果 x 无左子树
    • 直接访问 x,然后移动到右子树。
  2. 如果 x 有左子树
    • 找到左子树中最右边的节点(即前驱节点 predecessor)。
    • predecessor.right == null
      • 将其右指针指向 x(建立“线索”),然后进入左子树。
    • predecessor.right == x
      • 表示左子树已遍历完成,断开连接,访问 x,再进入右子树。

✅ 这种方法通过临时修改树结构实现了 O(1) 空间复杂度,最后恢复原状。

🧩 代码实现(带行注释):

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> res;
    TreeNode* predecessor = nullptr;

    while (root != nullptr) {
        if (root->left != nullptr) {
            // 寻找左子树中的最右节点(前驱)
            predecessor = root->left;
            while (predecessor->right != nullptr && predecessor->right != root) {
                predecessor = predecessor->right;
            }

            // 如果前驱的右指针为空,则建立“线索”
            if (predecessor->right == nullptr) {
                predecessor->right = root;  // 创建临时链接
                root = root->left;          // 向左子树深入
            } else {
                // 左子树已遍历完,断开链接
                predecessor->right = nullptr;
                res.push_back(root->val);   // 访问当前节点
                root = root->right;         // 转向右子树
            }
        } else {
            // 没有左子树,直接访问当前节点
            res.push_back(root->val);
            root = root->right;
        }
    }
    return res;
}

🧠 关键技巧:利用“前驱节点”的右指针作为“跳板”来回溯,避免使用额外空间。

⚙️ 复杂度分析:

  • 时间复杂度:O(n),每个节点最多被访问两次(一次建立线索,一次断开)。
  • 空间复杂度:O(1),仅使用常数级变量。

⚠️ 缺点:破坏了原始树结构(虽然后续恢复,但在多线程或不可变环境中不适用)。


🧩 解题思路(分步拆解)

步骤操作说明
定义目标:中序遍历 = 左 → 根 → 右
选择策略:
- 递归:天然匹配
- 迭代:手动控制栈
- Morris:空间极致优化
分析边界:
- 空树 → 返回空数组
- 单节点 → 输出其值
构建逻辑:
- 递归:先左后根再右
- 迭代:左走到头 → 弹出 → 访问 → 右转
- Morris:找前驱 → 建线索 → 断链接
验证正确性:用示例 [1,null,2,3] 手动模拟

✅ 推荐学习路径:先掌握递归 → 再理解迭代 → 最后挑战 Morris(进阶)


📈 算法分析对比总结

方法时间复杂度空间复杂度是否修改树是否易懂适用场景
递归O(n)O(h)✅ 高快速实现、面试初试
迭代O(n)O(h)✅ 中控制栈、防止栈溢出
MorrisO(n)O(1)✅ 是(临时)❌ 较难高级优化、空间敏感

💡 面试建议

  • 初面:写出递归 + 迭代即可。
  • 高阶面:能讲清楚 Morris 的原理,并指出其局限性(修改树结构)。

💻 代码(完整可运行模板)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 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:
    // 方法一:递归实现
    void inorder(TreeNode* root, vector<int>& res) {
        if (!root) {
            return;
        }
        inorder(root->left, res);
        res.push_back(root->val);
        inorder(root->right, res);
    }

    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        inorder(root, res);
        return res;
    }

    // 方法二:迭代实现
    vector<int> inorderTraversal_iterative(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        TreeNode* cur = root;

        while (cur != nullptr || !stk.empty()) {
            while (cur != nullptr) {
                stk.push(cur);
                cur = cur->left;
            }
            cur = stk.top();
            stk.pop();
            res.push_back(cur->val);
            cur = cur->right;
        }
        return res;
    }

    // 方法三:Morris 遍历
    vector<int> inorderTraversal_morris(TreeNode* root) {
        vector<int> res;
        TreeNode* predecessor = nullptr;

        while (root != nullptr) {
            if (root->left != nullptr) {
                predecessor = root->left;
                while (predecessor->right != nullptr && predecessor->right != root) {
                    predecessor = predecessor->right;
                }

                if (predecessor->right == nullptr) {
                    predecessor->right = root;
                    root = root->left;
                } else {
                    predecessor->right = nullptr;
                    res.push_back(root->val);
                    root = root->right;
                }
            } else {
                res.push_back(root->val);
                root = root->right;
            }
        }
        return res;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 构造测试用例:[1,null,2,3]
    TreeNode* root = new TreeNode(1);
    root->right = new TreeNode(2);
    root->right->left = new TreeNode(3);

    Solution sol;
    
    auto result1 = sol.inorderTraversal(root);
    cout << "递归结果: ";
    for (int x : result1) cout << x << " ";
    cout << endl;

    auto result2 = sol.inorderTraversal_iterative(root);
    cout << "迭代结果: ";
    for (int x : result2) cout << x << " ";
    cout << endl;

    auto result3 = sol.inorderTraversal_morris(root);
    cout << "Morris结果: ";
    for (int x : result3) cout << x << " ";
    cout << endl;

    return 0;
}

✅ 输出结果:

递归结果: 1 3 2 
迭代结果: 1 3 2 
Morris结果: 1 3 2 

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪