二叉树的遍历(前序、中序、后序、层序)
极难3走迷宫、拍照片、排座位——一次搞懂二叉树的四种遍历
你组织全班同学去参观一个神奇的“二叉树乐园”。乐园的每个景点(节点)最多只有两个岔路:左边和右边。你想把每个景点都打卡一遍,并且记录下打卡的顺序。你有四种不同的游览路线:
- 前序:“先拍照再探险”——到了岔路口,先掏出手机拍一张景点照,然后钻进左边小路,走到底后再回头走右边。
- 中序:“左边到底,回来拍照,再去右边”——先走进左边小路,一直走到不能再走,返回岔路口拍照,然后再走右边。
- 后序:“先玩遍两边,最后拍照”——先把左边和右边的小路都走完,最后回到岔路口补拍一张。
- 层序:“一层一层拍”——从入口开始,先拍完第一层的所有景点(从左到右),再下到第二层,以此类推。
这四种“游览路线”就是二叉树的四种遍历方式。遍历(Traversal)是指按照某种规则,访问树中的每一个节点恰好一次,从而把树这种“非线性”结构转换成一条“线性的”序列。不同遍历顺序得到的序列不同,适用于不同的场景。比如中序遍历二叉搜索树可以得到从小到大排序的序列;层序遍历常用于求树的宽度、最短路径等。
接下来,我们用下面这棵标准的二叉树来演示每种遍历。为了方便记忆,节点用大写字母标记。
A
/ \
B C
/ \ / \
D E F G
一、四种遍历的原理与过程
1. 前序遍历(Preorder):根 → 左 → 右
顺序:先访问当前节点(根),然后递归遍历左子树,最后递归遍历右子树。
过程详解(像不像“先拍照再向左走”?):
- 站在节点 A → 访问 A(拍照)
- 进入左子树(B 为根)→ 访问 B → 进入左子树(D 为根)→ 访问 D → D 的左右孩子都是空,返回 B
- 进入 B 的右子树(E 为根)→ 访问 E → 返回 B → 返回 A
- 进入右子树(C 为根)→ 访问 C → 进入左子树(F)→ 访问 F → 进入右子树(G)→ 访问 G
结果序列:A B D E C F G
生活例子:你走进一个迷宫,每到一个分岔口就立刻记录当前房间的名字,然后先探索左边的所有房间,再探索右边的。这样,每条路第一次遇到时就会被记下。
2. 中序遍历(Inorder):左 → 根 → 右
顺序:先递归遍历左子树,然后访问当前节点,最后递归遍历右子树。
过程详解(“先向左走到尽头,再回头拍中间,最后向右”):
- 从根 A 开始,先向左,进入子树 B → 再向左,进入子树 D → D 左空,访问 D(拍照)→ D 右空,返回 B
- 访问 B → 进入 B 的右子树 E → E 左空,访问 E → E 右空,返回 B → 返回 A
- 访问 A → 进入右子树 C → 向左进入 F → F 左空,访问 F → F 右空,返回 C
- 访问 C → 进入 C 的右子树 G → G 左空,访问 G → G 右空
结果序列:D B E A F C G
生活例子:你在一棵二叉树形状的书架上找书,每到一个书架就先查看左边抽屉里的书,再打开中间抽屉拍照,最后查看右边抽屉。这样拿到的书是按照“左-中-右”顺序排好的。
3. 后序遍历(Postorder):左 → 右 → 根
顺序:先递归遍历左子树,然后递归遍历右子树,最后访问当前节点。
过程详解(“左右全逛完,最后回来拍照”):
- 从 A 开始,向左进入 B → 向左进入 D → D 左右空,访问 D → 返回 B
- 进入 B 的右子树 E → E 左右空,访问 E → 返回 B → 访问 B → 返回 A
- 进入右子树 C → 向左进入 F → F 左右空,访问 F → 返回 C
- 进入 C 的右子树 G → G 左右空,访问 G → 返回 C → 访问 C → 返回 A → 访问 A
结果序列:D E B F G C A
生活例子:你清理房间,先打扫左房间的每个角落(全部完成),再打扫右房间,最后回到客厅拍一张完工照。这样“根”节点是最后一个被访问的。
4. 层序遍历(Level-order / BFS):从上到下,从左到右
顺序:按层从上到下,每层内从左到右依次访问。
过程详解(“就像排队报数,按层叫号”):
- 队列初始化,根 A 入队
- 出队 A → 访问 A → 左孩子 B 入队 → 右孩子 C 入队
- 出队 B → 访问 B → 左孩子 D 入队 → 右孩子 E 入队
- 出队 C → 访问 C → 左孩子 F 入队 → 右孩子 G 入队
- 出队 D → 访问 D → 无孩子
- 出队 E → 访问 E
- 出队 F → 访问 F
- 出队 G → 访问 G
结果序列:A B C D E F G
生活例子:班里的同学按身高站成二叉树形状,老师从第一排最左边开始点名字,然后第二排从左到右……一排一排地记录。
二、递归实现(C++ 和 Python)——代码已存在,这里补充理解要点
其实你已经在前面的 C++ 和 Python 代码中看到了递归版本。我们再来强调几个关键点:
- 递归出口:
if (root == nullptr) return;(C++)/if root is None: return(Python)。千万别漏掉这行,否则程序会无限递归直到栈溢出崩溃! - “访问”节点的位置:打印输出语句的位置决定了顺序。前序把输出放在递归前后;中序放在左递归之后、右递归之前;后序放在两个递归之后。
- 递归栈:递归其实背后用到了系统栈(调用栈),每递归一层像压一个栈帧。树很深时(例如 10000 层),栈可能爆掉(栈溢出)。这时可以考虑下面要讲的迭代版本。
记忆小口诀(很多同学靠这个记住顺序):
- 前序:根左右
- 中序:左根右
- 后序:左右根
- 层序:用队列,一层一层扫
三、迭代实现(用栈代替递归,避免栈溢出)
当二叉树深度很大(比如有 1 万层),递归会导致栈溢出。可以用显式栈来模拟递归过程。下面给出前序、中序、后序的迭代版本(层序本身就用队列,已经是迭代)。
3.1 前序遍历迭代(用栈,先压右孩子再压左孩子)
思路:访问根节点,然后先压右孩子、再压左孩子(因为栈是后进先出,保证左孩子先被处理)。
// C++ 前序迭代版本
#include <stack>
void preorderIterative(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
cout << node->data << " "; // 访问当前节点
// 先压右孩子,再压左孩子(这样左孩子先出栈)
if (node->right) st.push(node->right);
if (node->left) st.push(node->left);
}
}
# Python 前序迭代版本
def preorder_iterative(root):
if root is None:
return
stack = [root]
while stack:
node = stack.pop()
print(node.data, end=' ')
# 先压右,再压左,保证左先出
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
3.2 中序遍历迭代(用栈模拟“向左走到尽头”)
思路:从根开始,一直向左走,把沿途节点压入栈,直到左为空。然后出栈一个节点,访问它,再处理它的右子树。
void inorderIterative(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* curr = root;
while (curr != nullptr || !st.empty()) {
// 一直向左,把所有左节点压栈
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}
// 出栈,访问节点
curr = st.top();
st.pop();
cout << curr->data << " ";
// 转向右子树
curr = curr->right;
}
}
def inorder_iterative(root):
stack = []
curr = root
while curr or stack:
# 一路向左压栈
while curr:
stack.append(curr)
curr = curr.left
# 出栈访问
curr = stack.pop()
print(curr.data, end=' ')
# 转向右
curr = curr.right
3.3 后序遍历迭代(稍复杂,用两个栈或一个栈加标记)
经典方法:用两个栈。第一个栈按“根->右->左”入栈,第二个栈保存访问顺序,最后逆序输出。
void postorderIterative(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st1, st2;
st1.push(root);
while (!st1.empty()) {
TreeNode* node = st1.top();
st1.pop();
st2.push(node); // 把访问顺序暂存在 st2
if (node->left) st1.push(node->left);
if (node->right) st1.push(node->right);
}
// 从 st2 依次出栈得到后序序列
while (!st2.empty()) {
cout << st2.top()->data << " ";
st2.pop();
}
}
def postorder_iterative(root):
if root is None:
return
st1 = [root]
st2 = []
while st1:
node = st1.pop()
st2.append(node)
if node.left:
st1.append(node.left)
if node.right:
st1.append(node.right)
while st2:
node = st2.pop()
print(node.data, end=' ')
四、新手常犯的错误(附解决方法)
-
忘记递归出口
错误:void preorder(TreeNode* root) { cout << root->data; ... }没检查空指针。当树为空或递归到叶子节点的孩子时,程序崩溃。
解决:始终在函数最前面写if (root == nullptr) return;。 -
层序遍历时忘记检查左右孩子是否为空
错误:q.push(current->left);如果左孩子是 nullptr,会入队空指针,可能引发后续访问错误。
解决:加判断if (current->left) q.push(current->left);。 -
混淆三种递归顺序
症状:把中序遍历写成了前序,导致序列不对。
解决:用口诀“根左右、左根右、左右根”对照代码位置。 -
迭代版栈的顺序搞反
前序迭代:先压右再压左(左先出)。
中序迭代:记得先一路向左压栈。
后序迭代:用两栈法比较稳妥。 -
忘记层序使用队列
有人试图用栈实现层序,结果走出“螺旋”顺序。层序必须用队列(先进先出)。
五、完整可运行代码示例(带 main 和测试)
因为你的原始内容已经给了完整的 C++ 和 Python 代码(递归+层序),这里我补充一个包含递归和迭代所有四种遍历的完整 C++ 程序,并加上注释。Python 类似,不重复。
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
// 二叉树节点结构体
struct TreeNode {
char data; // 节点数据(用字符方便显示)
TreeNode *left; // 左孩子指针
TreeNode *right; // 右孩子指针
TreeNode(char val) : data(val), left(nullptr), right(nullptr) {}
};
// ---------- 递归遍历 ----------
void preorderRecursive(TreeNode* root) {
if (root == nullptr) return;
cout << root->data << " ";
preorderRecursive(root->left);
preorderRecursive(root->right);
}
void inorderRecursive(TreeNode* root) {
if (root == nullptr) return;
inorderRecursive(root->left);
cout << root->data << " ";
inorderRecursive(root->right);
}
void postorderRecursive(TreeNode* root) {
if (root == nullptr) return;
postorderRecursive(root->left);
postorderRecursive(root->right);
cout << root->data << " ";
}
// ---------- 迭代遍历 ----------
void preorderIterative(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
cout << node->data << " ";
if (node->right) st.push(node->right);
if (node->left) st.push(node->left);
}
}
void inorderIterative(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* curr = root;
while (curr != nullptr || !st.empty()) {
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}
curr = st.top();
st.pop();
cout << curr->data << " ";
curr = curr->right;
}
}
void postorderIterative(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st1, st2;
st1.push(root);
while (!st1.empty()) {
TreeNode* node = st1.top();
st1.pop();
st2.push(node);
if (node->left) st1.push(node->left);
if (node->right) st1.push(node->right);
}
while (!st2.empty()) {
cout << st2.top()->data << " ";
st2.pop();
}
}
// ---------- 层序遍历(队列)----------
void levelorder(TreeNode* root) {
if (root == nullptr) return;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* curr = q.front();
q.pop();
cout << curr->data << " ";
if (curr->left) q.push(curr->left);
if (curr->right) q.push(curr->right);
}
}
int main() {
// 构建标准二叉树
TreeNode* root = new TreeNode('A');
root->left = new TreeNode('B');
root->right = new TreeNode('C');
root->left->left = new TreeNode('D');
root->left->right = new TreeNode('E');
root->right->left = new TreeNode('F');
root->right->right = new TreeNode('G');
cout << "=== 递归版本 ===" << endl;
cout << "前序: "; preorderRecursive(root); cout << endl;
cout << "中序: "; inorderRecursive(root); cout << endl;
cout << "后序: "; postorderRecursive(root); cout << endl;
cout << "=== 迭代版本 ===" << endl;
cout << "前序: "; preorderIterative(root); cout << endl;
cout << "中序: "; inorderIterative(root); cout << endl;
cout << "后序: "; postorderIterative(root); cout << endl;
cout << "=== 层序 ===" << endl;
cout << "层序: "; levelorder(root); cout << endl;
// 释放内存(这里省略,实际可以使用递归 delete)
return 0;
}
输出如下(确保两种版本结果一致):
=== 递归版本 ===
前序: A B D E C F G
中序: D B E A F C G
后序: D E B F G C A
=== 迭代版本 ===
前序: A B D E C F G
中序: D B E A F C G
后序: D E B F G C A
=== 层序 ===
层序: A B C D E F G
六、总结与相关指引
- 遍历的意义:把二叉树的节点排成一条线,是许多高级操作的基础(如查找、打印、序列化、表达式求值)。
- 记忆方法:前中后序看根的位置;层序看队列。
- 时间与空间:所有遍历都是 O(n) 时间。递归空间 O(h)(h 为树高),层序空间 O(w)(w 为最大宽度)。
- 常见应用:
- 中序遍历二叉搜索树能得到升序序列(比如在搜索引擎中排序)。
- 前序遍历常用于复制树或表达式树的前缀表示(比如数学公式转成计算机指令)。
- 后序遍历用于删除树(先删除孩子再删除根)。
- 层序遍历用于求树的宽度、打印树形结构、最短路径等。
接下来你可以学习:
- 如何用遍历结果还原一棵树(已知前序+中序 或 后序+中序)
- 二叉树的深度与宽度计算
- 二叉搜索树(BST)中查找、插入、删除
- 堆(Heap)与优先队列(前序遍历就不适合堆了)
- 树的其他遍历:N 叉树的遍历、图的 DFS 和 BFS
试着画一棵简单的树(比如只有三个节点),手动模拟四种遍历,再用代码跑一遍,你会发现原来树遍历就像排队报数一样简单!
例题精讲
已知一棵二叉树的前序遍历序列为ABDCE,中序遍历序列为BDAEC,则该树的后序遍历序列是?
给定一棵二叉树的后序遍历序列和中序遍历序列,可以唯一确定该二叉树的结构。
补全下面递归中序遍历二叉树的代码。
void inorder(struct TreeNode* root) {
if (root == NULL) return;
inorder(root->left);
___
inorder(root->right);
}以下关于二叉树遍历的说法中,正确的是?
二叉树的层次遍历(层序遍历)序列唯一对应一棵二叉树。