📌 题目链接: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,分两种情况:
- 如果 x 无左子树:
- 直接访问 x,然后移动到右子树。
- 如果 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) | 否 | ✅ 中 | 控制栈、防止栈溢出 |
| Morris | O(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!💪