一、题目描述
给定一个二叉树的根节点 root,返回它的 中序遍历 结果。
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
示例
| 示例 | 输入 | 输出 |
|---|---|---|
| 示例1 | [1,null,2,3] |
[1,3,2] |
| 示例2 | [] |
[] |
| 示例3 | [1] |
[1] |
提示
- 树中节点数目在范围
[0, 100]内 -100 <= Node.val <= 100
进阶
递归算法很简单,你可以通过迭代算法完成吗?
二、解题思路总览
| 方法 | 核心思想 | 关键数据结构 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|---|
| 递归 | 左-根-右的递归顺序 | 函数调用栈 | O(n) | O(h),h为树高 |
| 迭代 | 模拟栈手动回溯 | 显式栈 | O(n) | O(h) |
递归与迭代的选择:
- 递归:代码简洁,但有函数调用开销
- 迭代:需要手动管理栈,但更通用
三、完整代码
方法一:递归
cpp
class Solution {
public:
// 递归函数:中序遍历
void dfs(TreeNode* root) {
if (!root) return; // 1. 空节点,返回
dfs(root->left); // 2. 递归遍历左子树
ans.push_back(root->val); // 3. 访问根节点
dfs(root->right); // 4. 递归遍历右子树
}
vector<int> ans; // 存放遍历结果
vector<int> inorderTraversal(TreeNode* root) {
dfs(root); // 调用递归函数
return ans; // 返回结果
}
};
方法二:迭代
cpp
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans; // 存放遍历结果
stack<TreeNode*> stk; // 模拟递归栈
while (root != NULL || !stk.empty()) {
// 1. 一直向左走,把经过的节点入栈
while (root != NULL) {
stk.push(root);
root = root->left;
}
// 2. 处理栈顶节点
root = stk.top();
stk.pop();
ans.push_back(root->val);
// 3. 切换到右子树
root = root->right;
}
return ans;
}
};
四、算法流程图(ASCII)
递归版流程
┌─────────────────────────┐
│ 调用 dfs(root) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ if (!root) return ? │
└───────────┬─────────────┘
Yes / \ No
/ \
▼ ▼
┌───────────┐ ┌─────────────────────────┐
│ 返回 │ │ dfs(root->left) │
└───────────┘ │ (递归左子树) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ ans.push_back(val) │
│ (访问根节点) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ dfs(root->right) │
│ (递归右子树) │
└─────────────────────────┘
迭代版流程
初始化:ans=[], stk=[], root指向根节点
┌───────────────────────────────────────┐
│ while循环开始 │
│ (root != NULL || !stk.empty()) │
└───────────────────┬───────────────────┘
│
┌───────────────────┴───────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ root != NULL ? │──── Yes ────→ │ stk.push(root) │
└────────┬─────────┘ │ root = root->left│
│ No └──────────────────┘
│ │
│ │ (循环向左)
│ │
│ ┌──────────────────────────────┘
│ │
│ │ ┌──────────────────┐
│ └─────┐ │ root = stk.top()│
│ │ │ stk.pop() │
│ │ │ ans.push_back() │
│ │ │ root = root->right│
│ │ └────────┬─────────┘
│ │ │
│ ▼ │
│ ┌──────────────────┴───┐
│ │ 返回循环继续 │
│ └──────────────────┘
▼
┌──────────────────┐
│ while循环结束 │
│ 返回 ans │
└──────────────────┘
五、逐行解析
递归版逐行解析
cpp
class Solution {
public:
void dfs(TreeNode* root) {
// ─────────────────────────────────────────
// 第1步:递归终止条件
// 空节点直接返回,不做任何处理
// ─────────────────────────────────────────
if (!root) return;
// ─────────────────────────────────────────
// 第2步:递归遍历左子树
// 中序遍历顺序:左-根-右,所以先遍历左子树
// ─────────────────────────────────────────
dfs(root->left);
// ─────────────────────────────────────────
// 第3步:访问根节点
// 左子树遍历完成后,将根节点值加入结果
// ─────────────────────────────────────────
ans.push_back(root->val);
// ─────────────────────────────────────────
// 第4步:递归遍历右子树
// 最后遍历右子树
// ─────────────────────────────────────────
dfs(root->right);
}
// ─────────────────────────────────────────
// 结果存储
// 作为成员变量,方便递归过程中累积结果
// ─────────────────────────────────────────
vector<int> ans;
vector<int> inorderTraversal(TreeNode* root) {
dfs(root); // 调用递归函数
return ans; // 返回遍历结果
}
};
迭代版逐行解析
cpp
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans; // 存放遍历结果
stack<TreeNode*> stk; // 显式栈,模拟递归
// ─────────────────────────────────────────
// 外层循环:处理完所有节点才结束
// root != NULL:有节点还没处理完
// !stk.empty():栈中还有待处理的节点
// ─────────────────────────────────────────
while (root != NULL || !stk.empty()) {
// ─────────────────────────────────────────
// 内层循环:一直向左走到尽头
// 目的:找到最左下的节点
// 同时把路径上的每个节点入栈保存
// ─────────────────────────────────────────
while (root != NULL) {
stk.push(root); // 入栈,保存节点以便后续回溯
root = root->left; // 继续向左走
}
// ─────────────────────────────────────────
// 此时root为NULL,已到达最左下的空节点
// 从栈中弹出最近保存的节点
// ─────────────────────────────────────────
root = stk.top(); // 栈顶节点
stk.pop(); // 弹出
// ─────────────────────────────────────────
// 处理栈顶节点(根节点)
// 这就是中序遍历的"根"步骤
// ─────────────────────────────────────────
ans.push_back(root->val);
// ─────────────────────────────────────────
// 切换到右子树
// 处理完根节点后,处理右子树
// 右子树会重复同样的过程
// ─────────────────────────────────────────
root = root->right;
}
return ans;
}
};
六、复杂度分析
时间复杂度
| 方法 | 分析 | 复杂度 |
|---|---|---|
| 递归 | 每个节点访问一次 | O(n) |
| 迭代 | 每个节点访问一次 | O(n) |
推导:
- 二叉树有 n 个节点
- 每个节点都会被访问一次(入栈一次、出栈一次)
- 因此时间复杂度是 O(n)
空间复杂度
| 方法 | 分析 | 复杂度 |
|---|---|---|
| 递归 | 函数调用栈,最大深度为树高h | O(h) |
| 迭代 | 显式栈,最大深度为树高h | O(h) |
推导:
- 树高为 h(h 取值 1~n)
- 最坏情况(链表形状):h = n,复杂度 O(n)
- 平衡树情况:h = log n,复杂度 O(log n)
七、面试追问 FAQ
| 问题 | 回答 |
|---|---|
| 递归和迭代哪个更好? | 递归代码简洁,迭代更通用。面试中可以先写递归,再写迭代展示能力。 |
| 递归的缺点是什么? | 有函数调用开销,可能导致栈溢出(树深度过大时)。 |
| 迭代如何避免栈溢出? | 用显式栈(堆内存)代替系统栈,但复杂度相同。 |
| 前序遍历和后序遍历的递归顺序是什么? | 前序:根-左-右。后序:左-右-根。 |
| 中序遍历有什么用? | BST(二叉搜索树)中序遍历得到有序序列。 |
八、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 94. 二叉树的中序遍历 | 简单 | 本题 |
| 144. 二叉树的前序遍历 | 简单 | 根-左-右顺序 |
| 145. 二叉树的后序遍历 | 简单 | 左-右-根顺序 |
| 98. 验证二叉搜索树 | 中等 | 中序遍历+有序性判断 |
| 230. 二叉搜索树中第K小的元素 | 中等 | BST中序遍历 |
| 94. 二叉树的中序遍历(迭代) | 简单 | 手动栈模拟 |
九、总结
| 对比项 | 递归 | 迭代 |
|---|---|---|
| 代码量 | 少(4行核心代码) | 多(需要手动管理栈) |
| 可读性 | 高,符合递归思维 | 较低,栈操作复杂 |
| 系统开销 | 函数调用开销 | 无函数调用 |
| 栈溢出风险 | 有(深度过深时) | 无(显式栈在堆上) |
| 面试建议 | 先写,展示基本功 | 进阶写,展示迭代能力 |
核心思想:
- 递归:系统帮你管理栈,代码简洁
- 迭代:手动模拟栈过程,需要两个循环(外层处理节点,内层一直向左)