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

二叉树的定义、性质与存储
从“每家最多两子”的家族分支,理解二叉树为什么是程序里最常用的树

掌握递归定义、5 条核心性质、满/完全二叉树的区别,以及顺序存储与链式存储两种存法。

6 大模块5 条性质2 种存储5 大易错点12+20 道练习
本讲 · 第 45/56 讲二叉树的定义、性质与存储
模块 01

情景导航

从'每个父亲最多有两个儿子'的家族分支,理解为什么二叉树是程序里最常用的树。

故事一个"每家最多两子"的家族

老王研究家谱时发现一个有趣的规矩:这一支的祖先规定,每个男子最多只能有两个儿子。长子继承家业,次子另立门户,再没有第三子。

于是家谱变成了这样:始祖只有一个;每个结点最多分出两个分支;并且长子和次子是有区别的——左边是老大,右边是老二。

每个结点最多两个孩子、左右有序、一个根 —— 这就是"二叉树"!🌳

把"人"换成"结点",把"父子"换成"边",就得到程序里的二叉树(Binary Tree)。和一般树相比,它多了一条铁律:每个孩子要么是左孩子、要么是右孩子,顺序不能乱。

💡 二叉树 ≠ 度为 2 的树

一般树只要求"每个结点最多 2 个孩子",但不区分左右;二叉树严格要求左右孩子有序——空的左孩子和空右孩子是两回事。这是两者最关键的差别。

①生活类比:二叉结构无处不在
⚖️

开关 / 决策树

是 / 否两个分支,一层层问下去,就构成一棵二叉树。

→ 每个结点两个选择
🏆

比赛对阵图

淘汰赛每一场产生一个胜者,胜负两两配对,天然是二叉树。

→ 左右子树
🔍

二分查找

每次与中点比较,往左或往右缩小区间,过程就是沿二叉树下行。

→ 有序下降
📂

哈夫曼编码

把字符按频率建成最优二叉树,左边 0、右边 1,得到压缩编码。

→ 左 0 右 1
②编程定义

二叉树(Binary Tree)是 n(n ≥ 0)个结点的有限集合。n = 0 时为空二叉树;n > 0 时:

① 有且仅有一个根结点;
② 其余结点分成两棵互不相交的左子树和右子树,左、右子树本身又都是二叉树。

注意"左、右"是有序的:即使某结点只有一个孩子,也要区分它是左孩子还是右孩子。这是二叉树和一般树最本质的区别。

📌 本讲核心知识地图

板块核心内容对应大纲
定义每个结点最多 2 孩子、左右有序、空二叉树二叉树的概念
性质层上限 2^(i-1)、总结点 2^k-1、n0=n2+1、深度⌊log2 n⌋+1二叉树的基本性质
特殊树满二叉树、完全二叉树特殊二叉树
存储顺序存储(数组)、链式存储(二叉链表)二叉树的存储结构

🔑 口诀一:二叉树,两边分,左是左来右是右,顺序乱了就不是它。

🔑 口诀二:叶子比"双子"多一个(n0 = n2 + 1);完全树按层排,2i、2i+1 找孩子。

模块 02

知识探究

掌握二叉树的递归定义、5 条核心性质、满/完全二叉树的区别,以及两种存储方式。

⭐二叉树的定义与结构

和普通树一样,二叉树也是递归定义的——"二叉树由左、右子树构成,子树还是二叉树"。下面是一棵有 6 个结点的二叉树:

示例二叉树(根 1,左子树 2,右子树 3)
第1层 第2层 第3层 1 2 3 4 5 6
根 1 分支 2、3 叶子 4、5、6 共 6 结点、5 边、3 层

📐 关于"空"的两种情形

① 空二叉树:一个结点都没有(n = 0),是合法二叉树;
② 只有根:n = 1,左右子树都为空;
③ 斜树:所有结点都只有左孩子(或都只有右孩子),退化成一条链——它仍是二叉树,但不是完全二叉树。

📐二叉树的 5 条重要性质

以下性质对任何二叉树都成立(i 从 1 开始计数层,k 为深度):

性质结论说明
性质1第 i 层最多 2^(i-1) 个结点每一层最多翻倍
性质2深度为 k 的二叉树最多 2^k - 1 个结点各层上限求和
性质3叶子数 n0 = n2 + 1(n2 为度为 2 的结点数)考试最高频!
性质4n 个结点的完全二叉树深度 = ⌊log2 n⌋ + 1向上取整也可写作 ⌈log2(n+1)⌉
性质5完全二叉树中,结点 i 的双亲是 ⌊i/2⌋,左孩子 2i,右孩子 2i+1i 从 1 开始编号

性质 5 是数组存储完全二叉树的理论基础,务必记牢。

🌟满二叉树 与 完全二叉树
满二叉树(Perfect)

每一层的结点数都达到最大,即深度为 k 时有 2^k - 1 个结点,且所有叶子都在最底层。

下标 i
1
2
3
4
5
6
7
结点值
1
2
3
4
5
6
7
每一层都满(2^k-1=7)→ 满二叉树
完全二叉树(Complete)

按层从上到下、同层从左到右连续填满,中间不能有空位;最后一层叶子靠左。

下标 i
1
2
3
4
5
6
结点值
1
2
3
4
5
6
按层从左到右连续填满,中间无空位 → 完全二叉树

💡 一句话区分

满二叉树是"每一层都满";完全二叉树是"按层从左到右排,排满前不跳空"。所以:满二叉树一定完全,完全二叉树不一定满。下例就不是完全二叉树:

下标 i
1
2
3
4
5
6
结点值
1
2
3
4
∧
6
第 5 位为空却有第 6 位 → 不是完全二叉树
👨顺序存储(数组表示法)

把完全二叉树的结点按层编号 1,2,3…,依次放进数组。借助性质 5,不用存指针就能算出任意结点的双亲和孩子——最适合完全二叉树。

C++array_store.cpp — 顺序存储(数组)访问双亲与孩子
#include <iostream>
using namespace std;
int main() {
    int a[] = {0, 1, 2, 3, 4, 5, 6};  // 下标从 1 开始,a[0] 弃用
    int n = 6;                         // 共 6 个结点
    cout << "i   双亲  左孩子  右孩子" << endl;
    for (int i = 1; i <= n; i++) {
        int p  = i / 2;                // 双亲 = ⌊i/2⌋
        int lc = 2 * i;                // 左孩子 = 2i
        int rc = 2 * i + 1;            // 右孩子 = 2i+1
        cout << i << "   " << p << "   " << (lc <= n ? lc : 0)
             << "   " << (rc <= n ? rc : 0) << endl;
    }
    return 0;
}
运行结果
i   双亲  左孩子  右孩子
1   0   2   3
2   1   4   5
3   1   6   0
4   2   0   0
5   2   0   0
6   3   0   0
🧒链式存储(二叉链表)

每个结点存自己的值和两个指针 lchild / rchild。一般二叉树(尤其非完全的)用链式更省空间、更灵活。

C++bt_chain.cpp — 二叉链表与先序建树
#include <iostream>
#include <string>
using namespace std;

struct TreeNode {                 // 二叉链表结点
    int val;
    TreeNode *lchild, *rchild;
    TreeNode(int v) : val(v), lchild(nullptr), rchild(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* node = new TreeNode(stoi(s));
    node->lchild = build(a, n);   // 递归建左子树
    node->rchild = build(a, n);   // 递归建右子树
    return node;
}

void preorder(TreeNode* root) {   // 先序:根-左-右
    if (!root) return;
    cout << root->val << " ";
    preorder(root->lchild);
    preorder(root->rchild);
}

int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    TreeNode* root = build(a, n);
    cout << "先序遍历:";
    preorder(root);
    cout << endl;
    return 0;
}
运行结果
先序遍历:1 2 4 5 3 6
📊两种存储方式对比
对比项顺序存储(数组)链式存储(二叉链表)
适用完全 / 满二叉树任意二叉树
找孩子O(1):2i、2i+1沿指针 O(1)
找双亲O(1):⌊i/2⌋需额外的父指针或遍历
空间紧凑,无指针开销每个结点 2 个指针
缺点非完全树会浪费大量空格指针占用额外内存
模块 03

实践应用

用性质与存储公式解决三道典型题:术语手推、性质应用、数组存储实战。

题1术语与手推

已知:上图示例二叉树(结点 1~6)。求结点数、边数、叶子数、深度、各结点的度。

解答:① 结点数 n = 6;② 边数 = n - 1 = 5(树的基本性质);③ 叶子 = {4,5,6} 共 3 个;④ 深度 = 3;⑤ 度:结点 1、2 度为 2,结点 3 度为 1,结点 4、5、6 度为 0。

验证性质3:n0 = 3,n2 = 2 → 3 = 2 + 1 ✔。

题2性质应用

已知:某二叉树有 n2 = 5 个度为 2 的结点,n1 = 3 个度为 1 的结点。求叶子数 n0 与总结点数 n。

解答:由性质 3,n0 = n2 + 1 = 6。总结点数 n = n0 + n1 + n2 = 6 + 3 + 5 = 14。也可验证:度数之和 = 2×5 + 1×3 = 13 = n - 1 = 13 ✔。

拓展:若已知度为 m 的树有 n_m 个、度为 1 的有 n_1 个,则 n0 = 1 + Σ(i-1)·n_i(i≥2),这是性质 3 在一般树上的推广。

题3数组存储实战

把完全二叉树(结点 1~6)放进数组,利用性质 5 写出每个结点的层号与祖先链。结点 i 的层号 = ⌊log2 i⌋ + 1,祖先链就是把 i 不断除以 2 直到 1。

C++level_ancestor.cpp — 数组存储下求层号与祖先链
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int a[] = {0, 1, 2, 3, 4, 5, 6};  // 完全二叉树,下标 1..6
    int n = 6;
    for (int i = 1; i <= n; i++) {
        int depth = (int)log2(i) + 1;       // 结点 i 所在层
        cout << "结点 " << i << ":第 " << depth << " 层,祖先链 ";
        int x = i;
        while (x > 0) { cout << x << " "; x /= 2; }
        cout << endl;
    }
    return 0;
}
运行结果
结点 1:第 1 层,祖先链 1
结点 2:第 2 层,祖先链 2 1
结点 3:第 2 层,祖先链 3 1
结点 4:第 3 层,祖先链 4 2 1
结点 5:第 3 层,祖先链 5 2 1
结点 6:第 3 层,祖先链 6 3 1
模块 04

总结提升

把定义、性质、特殊树、存储串成一张网,并了解与后续内容的联系。

📒核心要点总结
要点必须记住
定义每个结点最多 2 孩子,左右有序,可为空
性质1第 i 层最多 2^(i-1)
性质2深度 k 最多 2^k - 1 个结点
性质3n0 = n2 + 1(最常考)
性质4完全二叉树深度 = ⌊log2 n⌋ + 1
性质5双亲⌊i/2⌋、左2i、右2i+1(i 从 1 编号)
满 vs 完全满一定完全;完全按层从左排、不跳空
存储完全→数组(性质5);一般→二叉链表
🚀拓展:与后续内容的联系

① 遍历:下一讲(m5-06)会学先序 / 中序 / 后序 / 层序,它们是"访问每个结点一次"的系统方法,几乎一切二叉树算法都建立在遍历之上。

② 堆:完全二叉树顺序存储的极致应用就是堆(Heap)——父结点恒大于(或小于)孩子。它在排序、优先队列里极重要,属于提高级内容,CSP-J 了解即可。

③ 表达式树:算术表达式可以写成二叉树,先序 = 前缀表达式、后序 = 后缀表达式,这把"遍历"和"计算"直接连起来。

💭思考与启发

为什么"完全二叉树"这么受重视?因为它既保留了"2i、2i+1 立刻找孩子"的数组优势,又不浪费空间。很多高效数据结构(堆、线段树的数组实现)都建立在完全二叉树上。

动手试一试:画一棵 10 个结点的完全二叉树,写下数组下标,验证第 5 个结点的双亲是 2、左孩子是 10、右孩子不存在。你会对性质 5 印象极深。

模块 05

学习检测

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

⚠️5 大易错点
易错1

把"度为 2 的树"当成二叉树。一般树不区分左右,二叉树严格区分左右孩子,两者不等价。

易错2

性质 3 记反。正确是 n0 = n2 + 1(叶子比双子多 1),不是 n2 = n0 + 1,也不是 n1 参与。

易错3

数组下标从 1 还是 0。性质 5 的 2i、2i+1、⌊i/2⌋ 都要求根编号为 1;若从 0 开始,公式要整体改写。

易错4

满二叉树与完全二叉树混淆。"满"要求每层都满;"完全"只要求按层从左排且不跳空,满是完全的真子集。

易错5

深度公式算错。完全二叉树深度 = ⌊log2 n⌋ + 1;n = 6 时深度是 3 不是 2(因为 2^2 = 4 < 6)。

📋速查表
问题答案
第 i 层最多几个2^(i-1)
深度 k 最多几个2^k - 1
叶子与双子关系n0 = n2 + 1
完全树深度⌊log2 n⌋ + 1
孩子 / 双亲(从1编)左 2i、右 2i+1、父 ⌊i/2⌋
满二叉树判定n = 2^k - 1(每层都满)
总结点数n = 结点数 = 边数 + 1
📌笔记卡
定义

最多 2 孩子,左右有序,可空

性质3

n0 = n2 + 1

性质5

父⌊i/2⌋、左2i、右2i+1

满二叉树

每层都满,n = 2^k - 1

完全二叉树

按层从左排,不跳空

存储选择

完全→数组;一般→链表

课后练习

分层练习作业

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

🟢 A组 · 基础(4题)

A1选择题

下列关于二叉树的说法,正确的是()

A. 二叉树中每个结点可以有多个双亲
B. 度为 2 的树就是二叉树
C. 二叉树的左右子树是有序的
D. 空二叉树不是二叉树

查看参考答案

答案:C

A 错:每个非根结点只有一个双亲;B 错:一般树不区分左右,二叉树严格区分;D 错:空二叉树(n=0)是合法的二叉树。

考点:二叉树每个结点至多两个孩子,且严格区分左右孩子;“每个非根结点只有一个双亲”是所有树的共同性质,而空树也是合法的二叉树。

易错:把“一般树不区分左右”套到二叉树上,认为交换左右子树后还是同一棵树。

A2选择题

一棵二叉树有 n2 = 4 个度为 2 的结点,n1 = 2 个度为 1 的结点,则叶子数 n0 为()

A. 4   B. 5   C. 6   D. 7

查看参考答案

答案:B(5)

由性质 3:n0 = n2 + 1 = 4 + 1 = 5。

考点:由二叉树性质 3:n0 = n2 + 1 = 4 + 1 = 5。叶子数只由度为 2 的结点决定,与 n1 无关。

易错:把 n1 也算进公式,答成 4 + 2 + 1 = 7。

A3填空题

完全二叉树的第 5 层最多有 ______ 个结点;深度为 4 的二叉树最多有 ______ 个结点。

(请填写两个数字)

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

int main() {
    // 第 i 层最多 2^(i-1) 个结点;深度为 d 的二叉树最多 2^d - 1 个结点
    int level = 5;
    cout << "完全二叉树第 " << level << " 层最多 "
         << (1 << (level - 1)) << " 个结点" << endl;      // 16

    int depth = 4;
    cout << "深度为 " << depth << " 的二叉树最多 "
         << ((1 << depth) - 1) << " 个结点" << endl;      // 15
    return 0;
}

答案:第 5 层最多 16 个结点;深度为 4 的二叉树最多 15 个结点。

思路:二叉树每一层结点数最多翻倍:第 1 层 1 个、第 2 层 2 个、第 3 层 4 个……所以第 i 层最多 2^(i-1) 个,第 5 层最多 2^4 = 16 个。把各层加起来,深度为 d 的二叉树最多 1+2+4+…+2^(d-1) = 2^d − 1 个结点,深度 4 就是 2^4 − 1 = 15 个。

考点:二叉树的层结点数公式 2^(i-1)、总结点数公式 2^d − 1。

易错:别把"第几层"和"深度"搞混:第 i 层是 2^(i-1),而深度为 d 的整棵树最多是 2^d − 1。两个空一个问"某一层"、一个问"整棵树",答案 16 和 15 不能写反。

A4编程题

给定完全二叉树结点个数 n = 15,判断它是不是满二叉树,并输出深度。

提示:满二叉树满足 n = 2^k - 1。

查看参考答案

答案:n = 15 = 2^4 - 1,是满二叉树,深度 k = 4。

参考代码:

C++a4.cpp — 判断满二叉树
#include <iostream>
using namespace std;
int main() {
    int n = 15;
    int k = 0;
    while ((1 << k) - 1 < n) k++;
    cout << "n=" << n << " 深度=" << k
         << " 是否满二叉树:" << (n == (1 << k) - 1 ? "是" : "否") << endl;
    return 0;
}
运行结果
n=15 深度=4 是否满二叉树:是

思路:满二叉树的结点数一定形如 2^k - 1:用循环找最小的 k 使 2^k - 1 ≥ n,再看是否恰好取等——相等即满二叉树;完全二叉树的深度就是该 k。

易错:用 log2 求深度时忘记加 1,或对 2 的整数幂碰上浮点误差导致偶发判错。

🟡 B组 · 提高(4题)

B1选择题

有 100 个结点的完全二叉树,其深度为()

A. 6   B. 7   C. 8   D. 9

查看参考答案

答案:B(7)

深度 = ⌊log2 100⌋ + 1 = 6 + 1 = 7(因为 2^6=64 < 100 ≤ 2^7-1=127)。

考点:完全二叉树深度 k 满足 2^(k-1) ≤ n ≤ 2^k - 1,即 k = ⌊log2 n⌋ + 1;100 落在 64 与 127 之间,故 k = 7。

易错:直接取 ⌊log2 100⌋ = 6 当深度,忘了公式里还要 +1。

B2编程题

用先序串 1 2 4 # # 5 # # 3 # 6 # # 建一棵二叉树,统计度为 0 / 1 / 2 的结点数。

验证性质 3 是否成立。

查看参考答案

答案:n0 = 3,n1 = 1,n2 = 2,3 = 2 + 1 成立。

C++props_n0.cpp — 验证性质3:n0 = n2 + 1
#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 count(TreeNode* r, int& n0, int& n1, int& n2) {
    if (!r) return;
    int d = (r->l ? 1 : 0) + (r->r ? 1 : 0);  // 度:0 / 1 / 2
    if (d == 0) n0++; else if (d == 1) n1++; else n2++;
    count(r->l, n0, n1, n2);
    count(r->r, n0, n1, n2);
}
int main() {
    string a[] = {"1","2","4","#","#","5","#","#","3","#","6","#","#"};
    int n = sizeof(a) / sizeof(a[0]);
    TreeNode* root = build(a, n);
    int n0 = 0, n1 = 0, n2 = 0;
    count(root, n0, n1, n2);
    cout << "n0(叶子) = " << n0 << endl;
    cout << "n1(度为1) = " << n1 << endl;
    cout << "n2(度为2) = " << n2 << endl;
    cout << "性质3 验证:n0 = n2 + 1 -> " << n0 << " = " << n2
         << " + 1 " << (n0 == n2 + 1 ? "成立" : "不成立") << endl;
    return 0;
}
运行结果
n0(叶子) = 3
n1(度为1) = 1
n2(度为2) = 2
性质3 验证:n0 = n2 + 1 -> 3 = 2 + 1 成立

思路:先序串里遇到 # 就返回空,否则建结点并递归读出左右子树;建好后对每个结点数一数它有几个孩子:0 个记入 n0、1 个记入 n1、2 个记入 n2。

易错:读到 # 时忘了让位置指针也前进一格,后面整串字符会全部错位。

B3选择题

下列关于满二叉树与完全二叉树,错误的是()

A. 满二叉树一定完全二叉树
B. 完全二叉树一定满二叉树
C. 满二叉树叶子都在最底层
D. 完全二叉树最后一层叶子靠左

查看参考答案

答案:B

完全二叉树不一定满(如 6 个结点的完全二叉树不是满的)。A 正确:满是完全的真子集。

考点:满二叉树一定是完全二叉树;完全二叉树不一定满——结点数凑不满 2^k - 1 时,最后一行就是不满的。

易错:把包含关系记反,认为完全二叉树也一定满是。

B4填空题

在从 1 开始编号的完全二叉树数组中,第 10 个结点的双亲下标是 ______,左孩子下标是 ______(若存在)。

(填写两个数字)

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

int main() {
    // 从 1 开始编号的顺序存储:双亲 = i/2,左孩子 = 2i,右孩子 = 2i+1
    int n = 20;              // 假设这棵完全二叉树一共有 20 个结点
    int i = 10;

    cout << "结点 " << i << " 的双亲下标 = " << i / 2 << endl;    // 5
    cout << "结点 " << i << " 的左孩子下标 = " << 2 * i;
    if (2 * i > n)
        cout << "(已经超过总结点数 " << n << ",其实不存在)";
    cout << endl;                                                 // 20
    return 0;
}

答案:第 10 个结点的双亲下标是 5,左孩子下标是 20(只有当总结点数 ≥ 20 时它才真的存在)。

思路:完全二叉树用数组存储时(从 1 开始编号),编号 i 的结点双亲就是 ⌊i/2⌋,左孩子是 2i,右孩子是 2i+1。所以 10 号的双亲 = ⌊10/2⌋ = 5,左孩子 = 2×10 = 20。

考点:完全二叉树的顺序(数组)存储与下标关系。

易错:① 这套公式是"从 1 开始编号"的;若从 0 开始编号,双亲变成 (i−1)/2、左孩子变成 2i+1;② 算出来的孩子下标还要 ≤ n 才真实存在,不能直接当成有。

🔴 C组 · 挑战(4题)

C1证明题

试说明二叉树性质 3(n0 = n2 + 1)为什么成立。

(提示:从边数 = 结点数 - 1,以及度数之和 = 边数两个角度)

查看参考答案

证明:设总结点数 n = n0 + n1 + n2,边数 = n - 1。又边数也等于度数之和:n - 1 = 0·n0 + 1·n1 + 2·n2。两式相减:n0 + n1 + n2 - 1 = n1 + 2·n2 ⇒ n0 + n2 - 1 = n2 ⇒ n0 = n2 + 1。证毕。

思路:用两种方式数边:一方面边数 = 结点数 - 1,另一方面边数 = 度数之和 = 0·n0 + 1·n1 + 2·n2;两式联立消去 n 后即得 n0 = n2 + 1。

易错:忘记“边数 = n - 1”这条关键等式,只从度数之和出发就无法消元得结论。

C2应用题

某完全二叉树有 n 个结点,若用数组存储,求最后一个非叶子结点的下标,并说明原因。

(提示:最后一个非叶子结点一定是某个结点的双亲)

查看参考答案

答案:⌊n/2⌋

因为下标大于 ⌊n/2⌋ 的结点都没有孩子(2i > n),所以最后一个非叶子结点是 ⌊n/2⌋。这是堆排序"从下往上调整"的起点。

思路:编号 i 的结点有孩子当且仅当 2i ≤ n,所以凡是 i ≤ ⌊n/2⌋ 的结点都非叶子;编号再大的都满足 2i > n,只能当叶子,故最后一个非叶子结点就是 ⌊n/2⌋。

易错:答成 ⌈n/2⌉ 或 n/2 + 1,忘了判断有无孩子的依据是 2i 与 n 比较。

C3编程题

输入结点数 n,输出完全二叉树的深度,并判断是否满二叉树。

要求用 ⌊log2 n⌋ + 1 计算深度。

查看参考答案

答案(n = 10):深度 = 4,不是满二叉树(10 ≠ 2^4-1 = 15)。

C++c3.cpp — 深度与满二叉树判定
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int n = 10;
    int k = (int)log2(n) + 1;
    cout << "深度 = " << k << " 是否满二叉树:"
         << (n == (1 << k) - 1 ? "是" : "否") << endl;
    return 0;
}
运行结果
深度 = 4 是否满二叉树:否

思路:先由 k = ⌊log2 n⌋ + 1 求出深度,再检查 n 是否恰好等于 2^k - 1,相等就是满二叉树。

易错:用整数 log2 处理 2 的整数幂时会出现浮点误差(如 log2(8) 算出 3.0000001),建议改用循环试探或位运算判断。

C4开放题

为什么说“完全二叉树”是数组存储的最佳选择?斜树(所有结点都只有左孩子)用数组存会怎样?

(结合性质 5 与空间利用率思考)

查看参考答案

答案:完全二叉树按层连续编号,没有空位,数组不浪费;且性质 5 让双亲/孩子 O(1) 直达。斜树有 n 个结点却要占 2^(n-1) 个数组位置(几乎全空),空间浪费极端严重,因此斜树必须用链式存储。

思路:完全二叉树按层连续编号,正好与 1-based 数组一一对应、中间不留空位,并且双亲与孩子的下标可以直接算出,因此是数组存储的最佳形态。

易错:以为所有二叉树都适合数组存储;斜树有 n 个结点却要占 2^(n-1) 个位置,浪费极其严重。

🏆 洛谷原题(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对称二叉树提高-
15P1268树的重量提高
16P1040加分二叉树提高/省选-
17P2052[NOI2011] 道路修建提高
18P2899[USACO08JAN] Cell Phone Network提高
19P3379【模板】最近公共祖先(LCA)提高+/省选-
20P4323[JSOI2016] 扭动的树提高+/省选-