10 树与二叉树

GESP 6 级CSP-J / NOIP 高频CSP-S

级别标注:GESP 6 级(考级要求)/ CSP-J、NOIP 高频(竞赛进阶:遍历、表达式树、二叉堆、并查集树形结构)。

一、树的基本概念

树是非线性结构:有且仅有一个根(root);每个节点可能有多棵子树;n 个节点有 n-1 条边;无环连通。

术语:根、父节点、子节点、兄弟、叶子(无子)、度(子个数)、深度(根为 1 或 0)、高度。如二叉树度为 ≤2。

形象理解树像家族族谱:一个祖先(根)往下繁衍出各代子孙。和现实族谱不同,程序里的树规定“每个节点只有一个父节点”,所以不会出现“近亲联姻”的环。n 个节点的树恰好 n-1 条边,就像 n 个人的家族里,除了根每个人都有且仅有一个爸爸。
考级视角必背术语与公式:叶子 = 度 2 节点数 + 1(n0 = n2 + 1);深度 k 的二叉树最多 2^k - 1 个节点。GESP 6 级 / CSP-J 初赛都直接考这些公式,看到“度为 2 的节点有 5 个”要秒答叶子数 6。

二、二叉树(Binary Tree)

性质(必背): - 第 i 层最多有 2^(i-1) 个节点(i≥1)。 - 深度为 k 的二叉树最多有 2^k - 1 个节点。 - 任意二叉树:叶子数 = 度为 2 的节点数 + 1(n0 = n2 + 1)。 - 满二叉树:每层都满。 - 完全二叉树:除最后一层外全满,最后一层从左到右连续。

形象理解二叉树像公司组织架构图:每个领导最多管两个下属(左下属、右下属)。完全二叉树像最后一排有人请假的阶梯教室:前面几排坐满,最后一排从左往右连续坐人,不许有空位跳坐。
易错点满二叉树 vs 完全二叉树别混:满的“每层都满”;完全的“最后一层可能不满但必须从左到右连续”。笔试判断题“完全二叉树一定是满二叉树”是错的,反过来“满二叉树一定是完全二叉树”才对。

三、用数组/结构体存二叉树

C++ 实例
#include <iostream>
using namespace std;
const int N = 1005;
// 数组存法(适合完全二叉树):根在下标 1,左孩子 2*i,右孩子 2*i+1
int treeArr[N];

// 结构体/动态存法(一般二叉树,支持任意形态)
struct Node {
    int data;
    Node *left, *right;                     // 左右孩子指针
    Node(int d) : data(d), left(nullptr), right(nullptr) {}  // 构造
};

int main() {
    // 数组存法示例:根为 treeArr[1],其左孩子 2、右孩子 3
    treeArr[1] = 10;
    treeArr[2] = 20;
    treeArr[3] = 30;
    cout << "根" << treeArr[1] << " 左" << treeArr[2] << " 右" << treeArr[3] << endl;
    return 0;
}
形象理解数组存法像给每个节点编房间号:根住 1 号房,左孩子住 2 号、右孩子住 3 号,孩子房间号永远是父亲的 2 倍和 2 倍加 1——所以下标从 1 开始,从 0 开始就乱套了。
易错点数组存法只适合完全二叉树这类形态紧凑的树;歪歪扭扭的普通树用数组会浪费大量空间(很多空位)。普通二叉树用结构体 + 左右孩子指针更合适。

四、二叉树遍历(三种基础 + 层序)

C++ 实例
#include <iostream>
using namespace std;

struct Node { int data; Node *left, *right; };
// 以数组下标法演示,left_[u] = -1 表示 u 没有左孩子
int left_[10], right_[10];   // 存左右孩子下标

// 先序遍历:根 - 左 - 右
void pre(int u) {
    if (u == -1) return;          // 空节点返回
    cout << u << " ";             // 1) 访问根
    pre(left_[u]);                // 2) 遍历左子树
    pre(right_[u]);               // 3) 遍历右子树
}
// 中序遍历:左 - 根 - 右
void in(int u) {
    if (u == -1) return;
    in(left_[u]);                 // 1) 左子树
    cout << u << " ";             // 2) 根
    in(right_[u]);                // 3) 右子树
}
// 后序遍历:左 - 右 - 根
void post(int u) {
    if (u == -1) return;
    post(left_[u]);               // 1) 左子树
    post(right_[u]);              // 2) 右子树
    cout << u << " ";             // 3) 根
}
// 层序遍历:用队列逐层扫描(BFS 思想)
#include <queue>
void level(int root) {
    queue<int> q;
    q.push(root);                 // 根先入队
    while (!q.empty()) {
        int u = q.front(); q.pop();   // 取队首节点
        cout << u << " ";
        if (left_[u] != -1) q.push(left_[u]);    // 左孩子入队
        if (right_[u] != -1) q.push(right_[u]);  // 右孩子入队
    }
}

int main() {
    // 建一棵树:1 的左孩子 2,右孩子 3;2 的左孩子 4
    left_[1] = 2; right_[1] = 3;
    left_[2] = 4; right_[2] = -1;
    left_[3] = -1; right_[3] = -1;
    left_[4] = -1; right_[4] = -1;
    cout << "先序: "; pre(1); cout << endl;
    cout << "中序: "; in(1); cout << endl;
    cout << "后序: "; post(1); cout << endl;
    cout << "层序: "; level(1); cout << endl;
    return 0;
}

由中序+先序/后序可唯一还原二叉树(初赛笔试高频,写 reconstruct 是进阶题)。

形象理解三种遍历像不同的点名顺序:先序“报自己→点左边→点右边”;中序“先点左边→报自己→点右边”;后序“先点左边→点右边→最后报自己”。层序则像按楼层从上到下、从左到右点名(BFS)。
考级视角还原二叉树口诀:“先序/后序定根,中序分左右”。先序第一个是根,后序最后一个是根,再用中序把根左右两边切成左子树和右子树,递归下去。注意:只有先序 + 后序不能唯一还原(根的位置分不清左右)。

五、二叉树的应用

5.1 二叉查找树(BST)/ 二叉搜索树(概念)

左子树都 < 根,右子树都 > 根,中序遍历即升序。但退化为链时变 O(n),因此竞赛常用平衡树/SET。

5.2 堆(Heap)—— 完全二叉树实现优先队列

大根堆:父 ≥ 子;用小顶堆/大顶堆数组模拟(下标 1 起)。priority_queue 即堆的封装(第 7 章)。

5.3 哈夫曼树(Huffman / 最优二叉树)

带权路径长度最小的二叉树;构造:每次取两个最小权值合并,合并果子即是它的贪心体现。

5.4 表达式树

中缀表达式 a+b*c 可表示为树:根为最后计算的运算符 +,左右子树分别为 a 和 b*c。后序遍历表达式树即后缀表达式。

形象理解BST 像查字典:每翻一页就能排除一半(左小右大),查找快到 log n。但若按递增顺序插入,BST 会退化成“一根斜杆”,查起来又变回 O(n)——所以竞赛用平衡树/STL set。
考级视角表达式树常考:a+b*c 的根是最后计算的运算符 +。后序遍历表达式树 = 后缀表达式,这正好串起第 8 章的知识:后缀表达式求值就是表达式树的模拟计算。

六、树的遍历(多叉树 / 用邻接表)

C++ 实例
#include <iostream>
#include <vector>
using namespace std;
const int N = 100005;
vector<int> children[N];      // 邻接表存树:children[u] 存 u 的所有子节点

void dfs(int u) {             // 树的深度优先遍历(前序)
    cout << u << " ";         // 访问当前节点
    for (int v : children[u]) dfs(v);   // 依次递归每个孩子
}

int main() {
    children[1] = {2, 3};     // 1 的孩子是 2、3
    children[2] = {4, 5};     // 2 的孩子是 4、5
    dfs(1);                   // 从根 1 开始
    cout << endl;
    return 0;
}

树的深度 / 高度 / 子树大小均可通过一次 DFS 求出:

C++ 实例
int depth[N];                          // 记录每个节点的深度
// 求深度:父节点深度 + 1 传给子节点
void dfsDepth(int u, int d) {
    depth[u] = d;                      // 当前节点深度
    for (int v : children[u]) dfsDepth(v, d + 1);   // 孩子深度 +1
}
形象理解多叉树用邻接表存,像每个节点贴一张“孩子名单”:children[u] 就是 u 的孩子们列表,DFS 时逐个喊孩子递归。
易错点树 DFS 最容易漏父节点回溯判断:无向树遍历时,若不用 parent 参数记录“从哪来”,会沿着边跑回父节点造成死循环。有向树(只存孩子)则不会,看清题目给的是有向还是无向。

七、并查集(树形结构应用,NOIP/CSP 高频)

处理"分组/连通性"问题,路径压缩使均摊 O(α(n)):

C++ 实例
#include <iostream>
using namespace std;
const int N = 100005;
int fa[N];                              // fa[i] 记录 i 的父节点(并查集)
void init(int n) { for (int i = 1; i <= n; i++) fa[i] = i; }  // 各自成集合
int find(int x) {            // 找根 + 路径压缩(沿途节点直接指向根)
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int a, int b) {   // 合并两个集合
    int ra = find(a), rb = find(b);    // 找各自的根
    if (ra != rb) fa[ra] = rb;         // 不同根才合并
}
int main() {
    init(6);                 // 初始化 1~6 各自独立
    merge(1, 2);             // 1、2 合并
    merge(2, 3);             // 2、3 合并 → 1、2、3 同组
    cout << find(1) << " " << find(3) << endl;   // 同组根相同
    cout << (find(1) == find(3)) << endl;        // 1
    return 0;
}
形象理解并查集像班级认亲:每个人先自成一派(fa[i]=i),认识的人互相“认老大”(merge)。问两个人是不是一家人,就找各自“老大”(find)比一比。路径压缩像“每次认亲后直接记住最终老大”,下次再问一秒回答。
考级视角并查集模板必须背到条件反射:init + find(路径压缩)+ merge。find 用 fa[x] == x ? x : fa[x] = find(fa[x]) 一行写完。CSP-J 常考连通块数量:merge 完统计 fa[i] == i 的个数。

八、易错点 / 常见坑点

  • ⚠️ 遍历递归忘记返回值/终止条件或访问空指针(u == -1 / nullptr 要最先判断)。
  • ⚠️ 完全二叉树数组存法下标从 1 开始(0 的 2×0 还是 0),否则孩子下标全错。
  • ⚠️ 由序列还原树时,必须要有中序(只有先序+后序通常无法唯一还原)。
  • ⚠️ 层序遍历要预先入队根,每层处理要记录"当前层节点数"。
  • ⚠️ 并查集忘了 find 的路径压缩写成死循环(父指向自己判断要写清)。
  • ⚠️ n0 = n2 + 1 公式记死,笔试常考。

九、考级要求 vs 竞赛进阶

  • 考级要求(GESP 6 级):树的定义/术语、二叉树的三种遍历与层序、完全二叉树性质、表达式树的简单理解。
  • 竞赛进阶(CSP-J/NOIP):树的重建、树的深度/重心/直径 DFS、二叉堆手写、并查集(CSP-J 常考)、树的 DP(在 15 章延伸)、表达式树与后缀转换。

十、课内练习建议

  1. 手写先中后序遍历并输出,检查结果。
  2. 用递归求一棵树的高度和叶子数。
  3. 根据"中序+先序"手工还原一棵二叉树并画图。
  4. 用并查集解决"亲戚关系/连通块数量"问题。
  5. 手写大根堆的 push / pop(priority_queue 底层原理)。
本章目录
一、树的基本概念 二、二叉树(Binary Tree) 三、用数组/结构体存二叉树 四、二叉树遍历(三种基础 + 层序) 五、二叉树的应用 六、树的遍历(多叉树 / 用邻接表) 七、并查集(树形结构应用,NOIP/CSP 高频) 八、易错点 / 常见坑点 九、考级要求 vs 竞赛进阶 十、课内练习建议