NOI C++ 课程体系 / 数据结构篇 / 第6讲
CSP-J 入门级 · 树形结构 · m5-06

二叉树的遍历(前序 / 中序 / 后序)
从'按什么顺序访问每个结点'出发,彻底搞懂先序、中序、后序和层序

掌握递归与非递归实现、由先序+中序还原二叉树,以及表达式树与三种遍历的对应。

6 大模块4 种遍历5 大易错点12+20 道练习
本讲 · 第 46/56 讲二叉树的遍历
模块 01

情景导航

理解'按什么顺序访问每个结点'这件事,为什么是二叉树算法的基石。

故事查字典与走迷宫

老师让小明"把书架上的书全部看一遍"。小明犯了难:从哪本开始、按什么顺序?他可以"先看最左边一本,再往右",也可以"先挑最厚的,再挑薄的"。

二叉树也一样:我们有了树的结构,还得规定访问每个结点的顺序。这个"按规则把每个结点都走到一遍"的过程,就叫遍历(Traversal)。

每个结点恰好访问一次、且不遗漏 —— 这就是遍历 ✅

对二叉树,因为"根、左、右"三者顺序可换,最经典的有先序、中序、后序三种(加上层序共四种)。它们的名字就告诉我们:根在第几个被访问。

💡 为什么遍历如此重要

打印、统计、复制、删除、求深度……几乎所有二叉树操作,底层都是"先遍历一遍,边走边做事"。掌握了遍历,就掌握了操作二叉树的钥匙。

①生活类比:三种"先根后枝"
📂

整理文件夹

先处理当前文件夹(根),再进子文件夹(左),再进另一个(右) → 先序。

→ 根-左-右
🌳

修剪树枝

先把左侧枝全剪完,再回到主干剪自己,最后剪右侧 → 后序。

→ 左-右-根
🧮

算括号表达式

先算最里面的(左),再算外层(根),最后右边 → 中序对应中缀。

→ 左-根-右
📞

公司发通知

从总经理一层层往下通知到每个人 → 层序(BFS)。

→ 一层一层
②编程定义

遍历(Traversal):按照某种搜索路线,依次访问二叉树中每个结点恰好一次。

三种深度优先遍历(DFS)由"根"的位置命名:

名称顺序口诀
先序 Pre根 → 左 → 右(DLR)根在最前
中序 In左 → 根 → 右(LDR)根在中间
后序 Post左 → 右 → 根(LRD)根在最后

还有层序(Level):从根开始一层一层、同层从左到右访问(用队列实现)。

📌 本讲核心知识地图

板块核心内容对应大纲
三种DFS先序/中序/后序(递归)二叉树的遍历
层序队列BFS,一层一层二叉树的遍历
非递归用栈模拟递归(了解)栈的应用
还原先序+中序 唯一确定一棵二叉树遍历的综合应用

🔑 口诀一:先中后,看根的位;根前是先序,根中是中序,根后是后序。

🔑 口诀二:先序知根、中序分左右;两者一给,树就还原。

模块 02

知识探究

用示例树彻底搞懂先序/中序/后序/层序,并掌握递归、非递归、还原与表达式树。

⭐示例二叉树与三种遍历

下面这棵 6 结点的二叉树,将贯穿全讲。注意:左子树永远在右子树之前访问。

示例二叉树
第1层 第2层 第3层 1 2 3 4 5 6
根 1 叶子 4、5、6 示例树:1(2(4,5),3(,6))

按"根-左-右 / 左-根-右 / 左-右-根"三种顺序走一遍,得到:

三种遍历的访问顺序(第几步访问哪个结点)
先序
1第1步
2第2步
4第3步
5第4步
3第5步
6第6步
中序
4第1步
2第2步
5第3步
1第4步
3第5步
6第6步
后序
4第1步
5第2步
2第3步
6第4步
3第5步
1第6步
🔁递归实现(最核心)

三种遍历的递归写法只差一行打印的位置:打印在递归前=先序,在中间=中序,在最后=后序。这就是"遍历即递归"的精髓。

C++traverse_rec.cpp — 递归先/中/后序遍历
#include <iostream>
#include <string>
using namespace std;
struct TreeNode { int val; TreeNode *l,*r; TreeNode(int v):val(v),l(nullptr),r(nullptr){} };
int pos = 0;
TreeNode* build(const string a[], int n) {
    if (pos >= n) return nullptr;
    string s = a[pos++];
    if (s == "#") return nullptr;
    TreeNode* t = new TreeNode(stoi(s));
    t->l = build(a, n); t->r = build(a, n);
    return t;
}
void pre(TreeNode* r)  { if (!r) return; cout << r->val << " "; pre(r->l);  pre(r->r);  }
void in(TreeNode* r)   { if (!r) return; in(r->l);  cout << r->val << " "; in(r->r);   }
void post(TreeNode* r) { if (!r) return; post(r->l); post(r->r); cout << r->val << " "; }
int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    TreeNode* root = build(a, n);
    cout << "先序:"; pre(root);  cout << endl;
    cout << "中序:"; in(root);   cout << endl;
    cout << "后序:"; post(root); cout << endl;
    return 0;
}
运行结果
先序:1 2 4 5 3 6
中序:4 2 5 1 3 6
后序:4 5 2 6 3 1
🧭层序遍历(BFS)

层序不递归,而是用队列:从根入队,每次取出队首访问它,再把它的左右孩子入队。天然实现"一层一层、从左到右"。

C++level_order.cpp — 层序(BFS)遍历
#include <iostream>
#include <queue>
#include <string>
using namespace std;
struct TreeNode { int val; TreeNode *l,*r; TreeNode(int v):val(v),l(nullptr),r(nullptr){} };
int pos = 0;
TreeNode* build(const string a[], int n) {
    if (pos >= n) return nullptr;
    string s = a[pos++];
    if (s == "#") return nullptr;
    TreeNode* t = new TreeNode(stoi(s));
    t->l = build(a, n); t->r = build(a, n);
    return t;
}
void level(TreeNode* root) {
    if (!root) return;
    queue<TreeNode*> q; q.push(root);
    while (!q.empty()) {
        TreeNode* u = q.front(); q.pop();
        cout << u->val << " ";
        if (u->l) q.push(u->l);
        if (u->r) q.push(u->r);
    }
}
int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    TreeNode* root = build(a, n);
    cout << "层序:"; level(root); cout << endl;
    return 0;
}
运行结果
层序:1 2 3 4 5 6
📚非递归实现(用栈)

递归本质是用系统栈。手动用 stack 也能模拟:先序最直观——根入栈,循环取栈顶访问,先压右孩子、再压左孩子(因为栈是后进先出,左孩子要先出)。

C++nonrecur.cpp — 非递归先序(栈)
#include <iostream>
#include <stack>
#include <string>
using namespace std;
struct TreeNode { int val; TreeNode *l,*r; TreeNode(int v):val(v),l(nullptr),r(nullptr){} };
int pos = 0;
TreeNode* build(const string a[], int n) {
    if (pos >= n) return nullptr;
    string s = a[pos++];
    if (s == "#") return nullptr;
    TreeNode* t = new TreeNode(stoi(s));
    t->l = build(a, n); t->r = build(a, n);
    return t;
}
void pre_nr(TreeNode* root) {       // 非递归先序:用栈模拟递归
    if (!root) return;
    stack<TreeNode*> st; st.push(root);
    while (!st.empty()) {
        TreeNode* u = st.top(); st.pop();
        cout << u->val << " ";
        if (u->r) st.push(u->r);    // 右孩子后访问,先入栈
        if (u->l) st.push(u->l);
    }
}
int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    TreeNode* root = build(a, n);
    cout << "非递归先序:"; pre_nr(root); cout << endl;
    return 0;
}
运行结果
非递归先序:1 2 4 5 3 6

中序、后序的非递归写法较复杂,CSP-J 阶段掌握递归即可,非递归了解思路。

🔧由先序 + 中序 还原二叉树

只给先序和中序,就能唯一确定整棵树:先序第一个是根 → 在中序里找到根,左边是左子树、右边是右子树 → 对左右子树递归。下面直接输出还原后的后序。

C++rebuild.cpp — 已知先序+中序,还原二叉树
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 由先序 pre 与中序 in 重建二叉树,并输出后序
int pre_i = 0;
void rebuild(const vector<int>& in, int l, int r,
             const vector<int>& pre, string& post) {
    if (l > r) return;
    int root = pre[pre_i++];          // 先序第一个就是根
    int mid = l;
    while (in[mid] != root) mid++;     // 在中序里找到根的位置
    rebuild(in, l, mid - 1, pre, post);   // 递归左子树
    rebuild(in, mid + 1, r, pre, post);   // 递归右子树
    post += to_string(root) + " ";        // 最后访问根 → 后序
}
int main() {
    vector<int> pre = {1, 2, 4, 5, 3, 6};
    vector<int> in  = {4, 2, 5, 1, 3, 6};
    string post = "";
    rebuild(in, 0, (int)in.size() - 1, pre, post);
    cout << "后序:" << post << endl;
    return 0;
}
运行结果
后序:4 5 2 6 3 1
🧮表达式树:遍历即计算

算术表达式可以写成二叉树:运算符是根,操作数是叶子。于是 先序=前缀表达式、后序=后缀表达式(中序=中缀,但需加括号才无歧义)。

C++expr_tree.cpp — 表达式树与三种遍历
#include <iostream>
using namespace std;
struct TreeNode {
    char op; int val; bool isNum;
    TreeNode *l, *r;
    TreeNode(char o) : op(o), isNum(false), l(nullptr), r(nullptr) {}
    TreeNode(int v) : val(v), isNum(true), l(nullptr), r(nullptr) {}
};
void pre(TreeNode* r)  { if(!r) return; cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; pre(r->l);  pre(r->r);  }
void in(TreeNode* r)   { if(!r) return; in(r->l);  cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; in(r->r);   }
void post(TreeNode* r) { if(!r) return; post(r->l); post(r->r); cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; }
int main() {
    // 表达式 (1+2)*3 的二叉树
    TreeNode* root = new TreeNode('*');
    root->l = new TreeNode('+'); root->r = new TreeNode(3);
    root->l->l = new TreeNode(1); root->l->r = new TreeNode(2);
    cout << "前缀(先序):"; pre(root);  cout << endl;
    cout << "中缀(中序):"; in(root);   cout << endl;
    cout << "后缀(后序):"; post(root); cout << endl;
    return 0;
}
运行结果
前缀(先序):* + 1 2 3
中缀(中序):1 + 2 * 3
后缀(后序):1 2 + 3 *
模块 03

实践应用

三道典型题:手推遍历序列、由前+中还原、表达式树求值。

题1手推三种遍历

已知:示例二叉树(结点 1~6)。写出它的先序、中序、后序。

解答:先序 1 2 4 5 3 6(根 1 → 左子树 2,4,5 → 右子树 3,6);中序 4 2 5 1 3 6(左 4,2,5 → 根 1 → 右 3,6);后序 4 5 2 6 3 1(左 4,5,2 → 右 6,3 → 根 1)。与模块二程序输出完全一致。

小技巧:在中序序列里,某结点左边的全是它的左子树,右边的全是它的右子树——这是还原二叉树的关键。

题2由先序+中序还原

已知:先序 1 2 4 5 3 6,中序 4 2 5 1 3 6。求后序。

解答:先序首 1 是根;中序中 1 左边 {4,2,5} 是左子树、右边 {3,6} 是右子树。左子树先序为 2,4,5 → 2 是左根,其左 4、右 5;右子树先序 3,6 → 3 是右根,其右 6。还原即得后序 4 5 2 6 3 1。也可直接运行上面的 rebuild 程序验证。

题3表达式树求值

已知:表达式 (1+2)*3 的二叉树(根 *,左子树 +(叶子1、2),右叶子3)。

解答:先序=前缀 * + 1 2 3;中序=中缀 1 + 2 * 3(注意没有括号时运算顺序有歧义,真实中缀要加括号 (1+2)*3);后序=后缀 1 2 + 3 *。后缀表达式可直接用栈求值:1 2 入栈,遇 + 弹出 2、1 相加得 3 入栈,3 入栈,遇 * 弹出 3、3 相乘得 9。

模块 04

总结提升

把三种遍历、层序、还原、表达式树串成网,并了解与后续内容的联系。

📒核心要点总结
遍历顺序实现
先序根-左-右打印在递归前
中序左-根-右打印在递归中间
后序左-右-根打印在递归后
层序一层一层、左到右队列 BFS
还原先序定根、中序分左右递归重建
表达式树先序=前缀、后序=后缀遍历即输出
🚀拓展:与后续内容的联系

① 二叉搜索树(BST):若左子树全小于根、右子树全大于根,则中序遍历得到升序序列。这是排序、查找的基础,属提高级但思想极妙。

② 后缀表达式求值:后序遍历结果用栈 O(n) 求值,是编译原理的入门;理解了它,就理解了"栈+遍历"的配合。

③ 非递归遍历:中序、后序的手写栈实现在提高级常考,CSP-J 只需理解"递归=系统栈"这一层。

💭思考与启发

三种遍历只是"打印语句放哪儿"的区别——这启示我们:二叉树的很多算法,都是"在遍历的框架里,把要做的事安插在合适的位置"。先写对遍历,再往里填逻辑,思路会非常清晰。

动手试一试:把示例树的先序、中序、后序写下来,遮住中序,只用先序+后序能否还原?注意:先序+后序不能唯一确定二叉树,一定要中序(或已知是满二叉树)才行。

模块 05

学习检测

考试最爱挖的 5 个坑、一张速查表、6 张笔记卡,考前过一遍。

⚠️5 大易错点
易错1

搞混根的位置。先序根在最前、中序根在中间、后序根在最后;名字直接告诉你根在第几个。

易错2

递归三兄弟只改一行。把打印写错位置是通病:先序打印在两次递归前,后序在后,中序在中间。

易错3

以为先序+后序能还原树。只有先序+中序(或后序+中序)能唯一确定;先序+后序不行(除非是满二叉树)。

易错4

层序用栈。层序是 BFS,必须用队列;用栈会得到"根-右-左"之类的错误顺序。

易错5

中序当无歧义中缀。表达式树的中序遍历不含括号,直接当算式会算错,必须自行加括号。

📋速查表
遍历顺序根的位置
先序 Pre根-左-右第 1
中序 In左-根-右第 2
后序 Post左-右-根第 3
层序一层层左到右队列 BFS
还原树先序定根+中序分左右—
表达式先序=前缀、后序=后缀—
📌笔记卡
先序

根-左-右 DLR

中序

左-根-右 LDR

后序

左-右-根 LRD

层序

队列,一层层

还原

先序根+中序分

表达式

先缀/后缀遍历

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

对二叉树进行后序遍历,根结点被访问的时机是()

A. 最先   B. 中间   C. 最后   D. 不一定

查看参考答案

答案:C

后序是"左-右-根",根在最后访问。

考点:后序遍历的顺序是“左子树—右子树—根”,根结点最后才被访问。

易错:把后序当成“根—左—右”(那是先序),或与中序混淆。

A2选择题

下列遍历中,一定能由遍历序列唯一确定二叉树的是()

A. 先序+后序   B. 先序+中序   C. 仅先序   D. 仅后序

查看参考答案

答案:B

先序给出根、中序划分左右子树,二者配合可递归唯一确定树;先序+后序一般不行。

考点:先序能确定根、中序能据此划分左右子树,两者配合可以递归唯一确定一棵二叉树;先序配后序无法区分左右孩子,一般不行。

易错:认为“任意两种遍历组合都能唯一确定二叉树”,忽略了先序+后序的反例。

A3填空题

已知先序为 1 2 4 5 3 6、中序为 4 2 5 1 3 6,则后序为 ______。

(填写结点序列,空格分隔)

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
using namespace std;

struct node {
    char data;
    node *left, *right;
    node(char d) : data(d), left(nullptr), right(nullptr) {}
};

// 由先序 pre[l1..r1] 与中序 in[l2..r2] 建树
node* build(const string& pre, int l1, int r1,
            const string& in,  int l2, int r2) {
    if (l1 > r1) return nullptr;
    char root = pre[l1];                 // 先序的第一个就是根
    int k = in.find(root);               // 根在中序中的位置
    int len = k - l2;                    // 左子树的结点个数
    node* p = new node(root);
    p->left  = build(pre, l1 + 1,       l1 + len, in, l2,   k - 1);
    p->right = build(pre, l1 + len + 1, r1,       in, k + 1, r2);
    return p;
}

void postOrder(node* t) {                // 后序:左 → 右 → 根
    if (!t) return;
    postOrder(t->left);
    postOrder(t->right);
    cout << t->data << " ";
}

int main() {
    string pre = "124536";               // 先序序列
    string in  = "425136";               // 中序序列
    node* root = build(pre, 0, pre.size() - 1, in, 0, in.size() - 1);
    cout << "后序遍历:";
    postOrder(root);
    cout << endl;                        // 后序遍历:4 5 2 6 3 1
    return 0;
}

答案:后序序列是 4 5 2 6 3 1。

思路:先序的第一个数 1 一定是根;到中序里找 1,它左边 4 2 5 是左子树、右边 3 6 是右子树。对左右两棵子树重复同样的操作(先序里接着的数就是子树的根,中序负责把子树再分成左右两半),最后按"左 → 右 → 根"输出,就得到后序 4 5 2 6 3 1。顺便一提,这棵树的层序是 1 2 3 4 5 6。

考点:由先序遍历 + 中序遍历唯一确定一棵二叉树、三种遍历的相互关系。

易错:① 记住套路"先序找根、中序分左右";② 递归时区间边界容易差一位(左子树先序是 l1+1 ~ l1+len);③ 后序是最后才输出根,所以根 1 排在最后一个。

A4编程题

用先序串 1 2 4 # # 5 # # 3 # 6 # # 建树,输出中序序列。

提示:只需把遍历函数里的打印位置改到中间。

查看参考答案

答案:中序 4 2 5 1 3 6。

C++a4.cpp — 输出中序
#include <iostream>
#include <string>
using namespace std;
struct TreeNode { int val; TreeNode *l,*r; TreeNode(int v):val(v),l(nullptr),r(nullptr){} };
int pos = 0;
TreeNode* build(const string a[], int n) {
    if (pos >= n) return nullptr;
    string s = a[pos++];
    if (s == "#") return nullptr;
    TreeNode* t = new TreeNode(stoi(s));
    t->l = build(a, n); t->r = build(a, n);
    return t;
}
void inorder(TreeNode* r) { if (!r) return; inorder(r->l); cout << r->val << " "; inorder(r->r); }
int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    cout << "中序:"; inorder(build(a, n)); cout << endl;
    return 0;
}
运行结果
中序:4 2 5 1 3 6

思路:先按先序串递归建树(读到 # 返回空),再写一个中序递归:左子树 → 根 → 右子树,依次打印即可。

易错:打印时递归顺序写错(例如先打印根),结果输出的是先序而不是中序。

🟡 B组 · 提高(4题)

B1选择题

层序遍历二叉树通常使用的数据结构是()

A. 栈   B. 队列   C. 数组   D. 链表

查看参考答案

答案:B

层序是广度优先搜索 BFS,借助队列实现"访问一个、入队其孩子"。

考点:层序遍历是逐层从左到右访问,本质是广度优先搜索,需要队列来支撑“访问一个结点,就把它孩子按序入队”。

易错:误选栈——栈对应的是深度优先的三种遍历。

B2编程题

已知先序 1 2 4 5 3 6、中序 4 2 5 1 3 6,编程还原并输出后序。

(可参考模块二的 rebuild 思路)

查看参考答案

答案:后序 4 5 2 6 3 1。

C++rebuild.cpp — 已知先序+中序,还原二叉树
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 由先序 pre 与中序 in 重建二叉树,并输出后序
int pre_i = 0;
void rebuild(const vector<int>& in, int l, int r,
             const vector<int>& pre, string& post) {
    if (l > r) return;
    int root = pre[pre_i++];          // 先序第一个就是根
    int mid = l;
    while (in[mid] != root) mid++;     // 在中序里找到根的位置
    rebuild(in, l, mid - 1, pre, post);   // 递归左子树
    rebuild(in, mid + 1, r, pre, post);   // 递归右子树
    post += to_string(root) + " ";        // 最后访问根 → 后序
}
int main() {
    vector<int> pre = {1, 2, 4, 5, 3, 6};
    vector<int> in  = {4, 2, 5, 1, 3, 6};
    string post = "";
    rebuild(in, 0, (int)in.size() - 1, pre, post);
    cout << "后序:" << post << endl;
    return 0;
}
运行结果
后序:4 5 2 6 3 1

思路:先序的第一个元素就是根,在中序里定位它即可把中序切成左右两段,再对两段递归重建;建好后按“左—右—根”输出后序。

易错:递归时左右子树在中序里的区间边界算错(忘记跳过根结点本身),或没有在中序中搜索根的位置。

B3选择题

表达式 (1+2)*3 对应的后缀(逆波兰)表达式是()

A. 1 2 + 3 *   B. * + 1 2 3   C. 1 2 3 * +   D. + 1 2 * 3

查看参考答案

答案:A

后序遍历表达式树得到后缀表达式:1 2 + 3 *。

考点:把中缀表达式建成表达式树后:后序遍历得后缀(逆波兰)表达式,中序遍历得原中缀表达式,先序遍历得前缀表达式。

易错:把先序/后序与“前缀/后缀”的对应关系记反。

B4应用题

为什么“先序+后序”通常不能唯一确定一棵二叉树?举一个反例。

(提示:考虑只有一侧有孩子的情形)

查看参考答案

答案:因为先序和后序都只区分"根在两端",无法判断某结点是左孩子还是右孩子。反例:结点 1 只有左孩子 2,与结点 1 只有右孩子 2,二者的先序都是"1 2"、后序都是"2 1",却是两棵不同的树。

思路:先序只说明“根在最前”,后序只说明“根在最后”,两者都无法判断剩下的结点挂在左子树还是右子树。

易错:以为“能确定根就一定能确定整棵树”,忽略了单孩子结点有左、右两种挂法。

🔴 C组 · 挑战(4题)

C1证明题

说明:对二叉搜索树(BST)做中序遍历,得到的序列一定是升序的。

(提示:BST 满足左子树 < 根 < 右子树)

查看参考答案

证明:中序是"左-根-右"。由 BST 定义,左子树所有值 < 根 < 右子树所有值,递归下去,访问顺序正好是值的升序。这是 BST 最核心的性质。

思路:对任意结点,中序先完整遍历左子树(其中所有值都小于根),再访问根,最后遍历右子树(都大于根);左右两部分各自递归升序,拼起来就是整棵升序。

易错:只比较根与直接孩子的大小,没有用“整棵左子树都小于根”这一递归前提,无法保证全局升序。

C2应用题

写出把中序遍历改成“非递归”(用栈)的核心思路(不要求写完整代码)。

(提示:沿左链一路入栈,到底后处理栈顶)

查看参考答案

答案:从根开始,把所有"最左"结点依次入栈;弹出栈顶访问,若它有右孩子,则对右孩子重复"沿左链入栈"。栈空结束。这保证了"左-根-右"的顺序。

思路:用栈模拟递归:从当前结点出发把整条左链依次入栈;弹出栈顶访问后,若它有右孩子就转到右孩子,重复“沿左链入栈”;栈空即结束。

易错:弹出结点后忘记转向它的右子树,导致右子树整片漏访问。

C3编程题

构造表达式 (1+2)*3 的二叉树,输出它的前缀与后缀表达式。

(前缀=先序,后缀=后序)

查看参考答案

答案:前缀 * + 1 2 3,后缀 1 2 + 3 *。

C++expr_tree.cpp — 表达式树与三种遍历
#include <iostream>
using namespace std;
struct TreeNode {
    char op; int val; bool isNum;
    TreeNode *l, *r;
    TreeNode(char o) : op(o), isNum(false), l(nullptr), r(nullptr) {}
    TreeNode(int v) : val(v), isNum(true), l(nullptr), r(nullptr) {}
};
void pre(TreeNode* r)  { if(!r) return; cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; pre(r->l);  pre(r->r);  }
void in(TreeNode* r)   { if(!r) return; in(r->l);  cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; in(r->r);   }
void post(TreeNode* r) { if(!r) return; post(r->l); post(r->r); cout << (r->isNum?to_string(r->val):string(1,r->op)) << " "; }
int main() {
    // 表达式 (1+2)*3 的二叉树
    TreeNode* root = new TreeNode('*');
    root->l = new TreeNode('+'); root->r = new TreeNode(3);
    root->l->l = new TreeNode(1); root->l->r = new TreeNode(2);
    cout << "前缀(先序):"; pre(root);  cout << endl;
    cout << "中缀(中序):"; in(root);   cout << endl;
    cout << "后缀(后序):"; post(root); cout << endl;
    return 0;
}
运行结果
前缀(先序):* + 1 2 3
中缀(中序):1 + 2 * 3
后缀(后序):1 2 + 3 *

思路:把运算符作为内部结点、数字作为叶子建成表达式树,再做先序遍历得前缀表达式、后序遍历得后缀表达式。

易错:建树时把运算符与左右孩子的连接顺序接反,导致前缀/后缀表达式的运算对象颠倒。

C4开放题

有人说“遍历就是递归,递归就是遍历”。你怎么理解这句话?递归遍历可能带来什么问题?

(结合栈溢出、空间复杂度思考)

查看参考答案

答案:遍历天然符合"先处理子树再处理自己"的递归结构,所以三种 DFS 遍历写起来极像。但递归依赖系统栈,若树退化成链(深度 n),递归深度达 O(n),n 很大时可能栈溢出;此时需用非递归(手写栈)或改用迭代。这是"递归简洁但受限"的典型权衡。

思路:三种 DFS 遍历都是“先处理子树、再处理自己”的递归结构,所以代码长得几乎一样;但递归深度等于树高,斜树时可达 O(n),大 n 时可能栈溢出。

易错:认为递归遍历在任何输入下都安全,忽略了大 n 树退化成链时的爆栈风险。

🏆 洛谷原题(20道)

1B3642二叉树的遍历普及-
2P1305新二叉树普及-
3P4913【深基16.例3】二叉树深度普及-
4P1030求先序排列普及/提高-
5P1827[USACO3.4] 美国血统 American Heritage普及/提高-
6P3884[JLOI2009] 二叉树问题普及/提高-
7P1229遍历问题普及/提高-
8P1087FBI树普及/提高-
9P1364医院设置普及/提高-
10P2015二叉苹果树普及/提高-
11P2014[CTSC1997] 选课普及+/提高-
12P1395会议普及+/提高-
13P5836[USACO19DEC] Milk Visits S提高-
14P5018对称二叉树提高-
15P1040加分二叉树提高/省选-
16P1268树的重量提高
17P3379【模板】最近公共祖先(LCA)提高+/省选-
18P2052[NOI2011] 道路修建提高
19P2899[USACO08JAN] Cell Phone Network提高
20P4323[JSOI2016] 扭动的树提高+/省选-