二叉树的中序遍历 图解题解
中序遍历的递归版只有三行,但不用递归时怎么办?用栈模拟。
中序遍历就像把一棵树里每个节点的「待办事项」都交给了一摞便利贴:往左走一步就贴一张「回头还要访问我」,走到头了翻出最顶上那张访问它,然后转向它的右孩子继续贴新的。这摞便利贴就是栈,它帮你记住了所有「还没轮到但迟早要回来」的节点,让你不靠系统递归也能把顺序走对。
这道题到底在问什么
- 输入
- 二叉树如图
- 输出
- [8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 7]
最优解:为什么这么做
一句话答案:LeetCode 94 二叉树的中序遍历口诀是「左根右」:递归版三行写完;面试更常追问的迭代版用显式栈——从当前节点一路向左把沿途节点压栈,弹出栈顶即访问,再转向它的右子树重复。每个节点恰好入栈、出栈各一次,时间 O(n),空间 O(H),H 为树高。
中序遍历的顺序到底是什么
中序遍历要求对每个节点都遵守同一条规则:左子树全部访问完,才轮到根,最后才是右子树。「中」指的是根被夹在中间输出。这个定义是递归的——访问左子树时,内部同样按左根右展开。由此能直接推出一个结论:整棵树第一个被访问的节点,一定是从根一路沿左孩子走到底的那个最左下节点;最后一个则是最右下节点。
递归写法为什么天然正确
递归版几乎是定义的直译:先调用 inorder(root.left),再收集 root.val,最后调用 inorder(root.right)。「左子树必须先处理完」这条约束不需要任何额外代码来保证——函数调用栈替你保证了:inorder(root.left) 不返回,后面两行根本不会执行。思路上递归是最清晰的版本,但面试常追问「不用递归怎么写」,所以显式栈的迭代版才是本题的重点。
迭代版为什么要一路向左压栈
不用递归,就得自己接管调用栈的职责,这正是显式栈的作用。从当前节点出发一路向左,把沿途节点依次压栈:栈里存的是「左子树还没处理完、暂时不能访问」的节点,同时也是之后往回走的路。走到最左尽头时,栈顶恰好是所有未访问节点中最靠左的那个——它没有左孩子或左子树已清空,按左根右的规则,此刻正该轮到它。
弹出即访问,凭什么成立
整个迭代过程维持一条不变量:任何节点被弹出时,它的左子树必然已经全部访问完。原因很直接——节点入栈之后,算法先钻进它的左子树,左边全部处理完毕才可能弹到它。所以弹出即可放心访问,访问完把指针转向右子树,对右子树再重复「一路向左压栈」。循环条件写成栈非空或当前指针非空,两者都空才说明整棵树走完。
复杂度与常见的翻车点
时间 O(n):每个节点恰好入栈一次、出栈一次,n 为节点数。空间 O(H):栈里最多同时存一条从根到当前位置的左链,H 为树高,链状树最坏 O(n)。两个高频错误:把顺序写成根左右,那是前序遍历,结果整体错位;迭代时压栈方向搞反,弹出的就不再是当前最左的未访问节点。若被追问 O(1) 空间的做法,可以提 Morris 中序遍历:借叶子的空右指针临时指回后继节点,遍历完再拆掉线索,时间仍 O(n),空间降到 O(1),属进阶考点。
▶ 动画逐步走查(共 42 步)——想跟着动画一帧帧对照就展开
- 3核心一句话:中序 = 左根右。左子树全访问完才轮到根,再右子树。
- 4完整的二叉树。中序遍历从根开始,一路沿左孩子向下走到最左下角的节点,那里就是第一个被访问的节点。
- 5怎么找下一个?从根一路向左压栈,走到最左下角。当前定位到节点 8——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 6正在访问节点 8(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 7节点 8 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 8怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 4——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 9正在访问节点 4(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 10节点 4 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 11怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 9——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 12正在访问节点 9(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 13节点 9 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 14怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 2——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 15正在访问节点 2(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 16节点 2 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 17怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 10——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 18正在访问节点 10(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 19节点 10 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 20怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 5——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 21正在访问节点 5(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 22节点 5 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 23怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 11——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 24正在访问节点 11(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 25节点 11 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 26怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 1——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 27正在访问节点 1(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 28节点 1 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11, 1]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 29怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 12——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 30正在访问节点 12(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 31节点 12 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11, 1, 12]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 32怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 6——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 33正在访问节点 6(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 34节点 6 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11, 1, 12, 6]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 35怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 13——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 36正在访问节点 13(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 37节点 13 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 38怎么找下一个?左子树已全部访问完,弹回栈顶的父节点。当前定位到节点 3——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 39正在访问节点 3(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 40节点 3 访问完,加入中序结果(蓝色=已访问)。中序结果现在是 [8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3]。接着去访问它的右子树(同样先访问右子树里最左的节点)。
- 41怎么找下一个?进入上一节点的右子树后,再一路向左到底。当前定位到节点 7——它的左子树(如果有)已经全部访问完了,所以现在轮到它。
- 42正在访问节点 7(橙色)。中序的规则是「左 → 根 → 右」:它的左子树已经处理完,现在把这个「根」输出。
- 43节点 7 访问完,加入中序结果(蓝色=已访问)。整棵树的中序遍历完成,结果是 [8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 7]。
- 44全部 13 个节点都按「左 → 根 → 右」访问完了(绿色)。最终中序遍历结果 = [8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 13, 3, 7]。每个节点都满足:它的整个左子树排在它前面、整个右子树排在它后面。
⚠️ 容易写错的地方
✗ 错:顺序写成 根 → 左 → 右(前序)
✓ 对:中序是 左 → 根 → 右
只有「左→根→右」才是中序,顺序错了结果就错
✗ 错:没等左子树访问完就先输出根
✓ 对:左子树必须全部访问完才轮到根
中序的核心约束就是「左在根前」
✗ 错:迭代时压栈方向写反(先压右)
✓ 对:一路向左压栈,弹出再转右
保证每次弹出的都是当前最左、未访问的节点
完整代码(Python / C++ / Java)
Python
def inorderTraversal(root):
res, stack = [], []
cur = root
while stack or cur:
while cur: # 一路向左压栈
stack.append(cur)
cur = cur.left
cur = stack.pop() # 弹出即访问(左已处理完)
res.append(cur.val) # 访问根
cur = cur.right # 再转向右子树
return resC++
vector<int> inorderTraversal(TreeNode* root){
vector<int> res;
stack<TreeNode*> st;
TreeNode* cur = root;
while(!st.empty() || cur){
while(cur){ st.push(cur); cur = cur->left; }
cur = st.top(); st.pop();
res.push_back(cur->val);
cur = cur->right;
}
return res;
}Java
public List<Integer> inorderTraversal(TreeNode root){
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while(!stack.isEmpty() || cur != null){
while(cur != null){ stack.push(cur); cur = cur.left; }
cur = stack.pop();
res.add(cur.val);
cur = cur.right;
}
return res;
}复杂度
时间
O(n)
每个节点恰好入栈、出栈各一次
空间
O(H)
栈最多存一条从根到叶的路径,H 为树高(最坏 O(n))
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树的中序遍历 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
递归和迭代两种写法,面试更推荐哪个?+
思路上递归最清晰:inorder(left) → 访问 root → inorder(right),三行就讲完。但面试常追问「不用递归怎么写」,所以迭代版(显式栈)也要会,原理是手动模拟递归调用栈。
有没有 O(1) 额外空间的中序遍历?+
有,Morris 中序遍历:利用叶子节点的空右指针建立「线索」临时指回后继,遍历完再拆掉。时间仍 O(n),空间降到 O(1),但会临时改动树结构,是进阶考点。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二叉树的中序遍历 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。