力扣刷题#25-0094-二叉树的中序遍历
力扣刷题#25-0094-二叉树的中序遍历
题目
给定一个二叉树的根节点 root,返回它的中序遍历。
输入: root = [1,null,2,3]
1
\
2
/
3
输出: [1,3,2]
前置知识:三种 DFS 遍历的唯一区别
递归遍历二叉树的框架完全相同,只差一行 cout(或 push_back)的位置:
void dfs(TreeNode* root, vector<int>& res) {
if (root == nullptr) return;
// ① 前序:res.push_back(root->val) 放这里 → "中左右"
dfs(root->left, res);
// ② 中序:res.push_back(root->val) 放这里 → "左中右"
dfs(root->right, res);
// ③ 后序:res.push_back(root->val) 放这里 → "左右中"
}
同一棵树,跑三套代码的唯一区别就是把那行 push_back 挪到不同位置。
我的递归版代码
class Solution {
public:
void dfs(TreeNode* root, vector<int>& res) {
if (root == nullptr) return;
dfs(root->left, res);
res.push_back(root->val);
dfs(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
dfs(root, res);
return res;
}
};
套中序模板:先跑左子树 → 输出当前节点 → 再跑右子树。
从递归到迭代的思考
递归版写起来很爽——dfs(root->left) 一行代码,函数调用栈自动帮我记"返回时该回到哪一层"。
迭代版没了函数调用栈,我需要自己管理这个"回到哪一层"的信息。工具就是 stack<TreeNode*>。
中序迭代的核心逻辑
1. 从当前节点出发,一路向左,边跑边把经过的节点全压栈
2. 左边走到底(cur == nullptr)→ 弹栈顶 → 这个节点就是"当前最左未处理"的 → 输出它
3. 转向这个节点的右子树,重复上述过程
用一棵具体树走一遍
1
/ \
2 3
/ \ \
4 5 6
/
7
| 步骤 | 栈 | cur | 操作 |
|---|---|---|---|
| 从 1 出发 | [1] |
2 | 1 压栈,去左 |
[1,2] |
4 | 2 压栈,去左 | |
[1,2,4] |
null | 4 压栈,去左→空了 | |
| 左边没路 | [1,2] |
— | 弹 4,输出 4,转向 4 的右→null |
[1] |
— | 弹 2,输出 2,转向 5 | |
[1,5] |
7 | 5 压栈,去左 | |
[1,5,7] |
null | 7 压栈,去左→空 | |
[1,5] |
— | 弹 7,输出 7 | |
[1] |
— | 弹 5,输出 5 | |
[] |
— | 弹 1,输出 1,转向 3 | |
[3] |
null | 3 压栈,左空 | |
[] |
— | 弹 3,输出 3,转向 6 | |
[6] |
null | 6 压栈,左空 | |
[] |
— | 弹 6,输出 6 | |
| 全空 | [] |
null | 结束 |
输出:4, 2, 7, 5, 1, 3, 6,和递归版一致。
我的迭代版代码
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != nullptr || !st.empty()) {
while (cur != nullptr) {
st.push(cur);
cur = cur->left;
}
cur = st.top();
st.pop();
res.push_back(cur->val);
cur = cur->right;
}
return res;
}
};
代码逐段解析
第 5-6 行:初始化
stack<TreeNode*> st;
TreeNode* cur = root;
st 模拟递归调用栈,cur 表示"当前正在处理的节点指针"。递归版这个指针藏在函数参数里,迭代版我手动维护它。
第 8 行:外层循环条件
while (cur != nullptr || !st.empty()) {
这是整段代码最容易困惑的一行。两个条件是 ||(或),缺一不可。
为什么需要 cur != nullptr?
弹完 1、转向右子树 3 的瞬间:
stk = [] ← 栈空了
cur = 3 ← 但 3 还没处理
如果只有 !st.empty() → false → 循环退出 → 3 和 6 全丢了。cur 还有活、栈没活,靠 cur != nullptr 续命。
为什么需要 !st.empty()?
一路向左走到 4 之后:
cur = nullptr ← 指针空了
stk = [1,2,4] ← 但栈里还有三个等着
如果只有 cur != nullptr → false → 循环退出 → 2, 5, 1, 3, 6 全丢了。栈有活、cur 没活,靠 !st.empty() 续命。
| 状态 | cur | stk | 还跑? | 续命条件 |
|---|---|---|---|---|
| 向左走到底 | null | 有数据 | ✅ | !stk.empty() |
| 弹完转右子树 | 指向新节点 | 可能空 | ✅ | cur != null |
| 全处理完 | null | [] |
❌ 结束 | 两个都 false |
第 9-12 行:内层 while —— 一路向左
while (cur != nullptr) {
st.push(cur);
cur = cur->left;
}
和递归 dfs(root->left) 对应。把沿途每个节点都压栈——它们都是"还没处理的中序根节点",等着左子树跑完再回来输出。
第 13-16 行:弹栈 → 输出 → 转右
cur = st.top();
st.pop();
res.push_back(cur->val);
cur = cur->right;
| 操作 | 对应递归的 |
|---|---|
cur = st.top(); st.pop() |
函数调用返回(自动回到上层) |
res.push_back(cur->val) |
中序的 cout |
cur = cur->right |
dfs(root->right) |
弹出来的节点左子树已经全处理完了,所以立即输出自身,然后转向右子树。
递归 vs 迭代对照
| 递归 | 迭代 |
|---|---|
dfs(root->left) |
st.push(cur); cur = cur->left |
| 函数自动返回上层 | cur = st.top(); st.pop() |
res.push_back(root->val) |
res.push_back(cur->val) |
dfs(root->right) |
cur = cur->right |
复杂度分析
| 维度 | 递归 | 迭代 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(h),递归栈 | O(h),显式栈 |
| 优点 | 代码最短 | 不依赖递归,可避栈溢出 |
| 缺点 | 深层树可能爆栈 | 代码稍长 |
本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号