NOI C++ 课程体系 / 数据结构篇 / 第七讲
数据结构 · 完全二叉树

完全二叉树与数组表示法
只用一维数组,就存下整棵树

掌握 2i / 2i+1 / ⌊i/2⌋ 的编号魔法,理解完全二叉树为什么能“数组化”,并初识堆。

核心 2i·2i+1·⌊i/2⌋约定 1-based / 0-based关系 满 ⊂ 完全关键 最后非叶子 ⌊n/2⌋拓展 堆 = 完全二叉树
本讲 · 第 47/56 讲完全二叉树与数组表示法
01

情景导航

从“给书架编号”出发,理解为什么完全二叉树可以只用一个数组表示,以及它带来的编号魔法。

📖 引入故事:图书馆的“智能书架”

学校图书馆有 1000 排书架。管理员想给每排书架编个号,使得只要知道编号 i,就能立刻算出它的上一排(双亲)和左右两排(孩子)。

普通编号(1,2,3…随便排)做不到这一点。但如果按“一层一层、从左到右”的方式编号——就像完全二叉树那样——神奇的事发生了:

  • 双亲编号 = ⌊i / 2⌋(向下取整)
  • 左孩子 = 2 × i,右孩子 = 2 × i + 1

也就是说:树的结构不用“指针”,只用数组下标就全记下来了。这就是本讲的主角——完全二叉树的数组表示法。

🎯 生活类比:家庭合影的站位

全家拍照,规定“长辈站第一排,子女站父母正下方、先左后右”。如果每一排都站满、最后一排从左往右连续站,就是“完全”的站位;中间一旦有人缺席又有人在更右边,就“不完全”了。完全二叉树就是这个规则。

这种“填满再开下一层”的规则,让我们可以用一个数组 a[1..n] 把整张“合影”存下来,而且任意人的位置都能由编号算出。

📝 编程定义

完全二叉树(Complete Binary Tree):一棵深度为 k 的二叉树,其前 k-1 层都是满的(每个结点都有两个孩子),第 k 层(最后一层)的结点从左往右连续排布、中间没有空位。

数组表示法:按层序(从上到下、同层从左到右)给结点编号 1,2,…,n,存入数组 a[1..n]。则结点 i 的双亲、左孩子、右孩子的下标分别是 ⌊i/2⌋、2i、2i+1(1-based)。

核心约定1-based:a[0] 弃用;0-based 见知识探究卡片 3

🔑 核心口诀

“完全才能数组存,2i、2i+1 管孩子,⌊i/2⌋ 管双亲;满二叉树是特例,堆也靠它立。”

🗺️ 本讲知识地图

知识点你要掌握难度
完全二叉树定义前 k-1 层满、最后一层左连续★
数组编号公式(1-based / 0-based)双亲 ⌊i/2⌋、孩子 2i/2i+1★
满二叉树 vs 完全二叉树满 ⊂ 完全;反之不成立★
深度与最后非叶子结点k=⌊log₂n⌋+1;⌊n/2⌋★★
判断完全二叉树层序遇空后再有非空 → 否★★
堆(大/小顶堆)与数组满足堆性质的完全二叉树(拓展)★★★
02

知识探究

完全二叉树为什么能“只用一个数组”表示?看编号公式、1-based/0-based 差异、满与完全的关系,以及数组里的堆。

① 什么是完全二叉树(看图)

下面两棵树同样是 10 / 约 10 个结点,但只有左边是完全二叉树。

图1 · 一棵完全二叉树(10 个结点)
1
2
3
4
5
6
7
8
9
10
前 3 层全满,第 4 层从左往右连续无空位 → 完全二叉树
图2 · 不是完全二叉树(出现“空位”)
1
2
3
4
5
7
第 3 层中间“6”缺失(空位),但后面仍有“7” → 不是完全二叉树

判断口诀:从根开始层序编号,一旦遇到“空位”(某个该有的孩子不存在),后面所有位置都必须也是空;否则就不是完全二叉树。

② 数组存储的“编号魔法”

把上图完全二叉树按层序存入 a[1..10],观察每个结点的双亲、左右孩子在数组里的下标:

C++完全二叉树·数组存储(1-based)
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 1-based,a[0] 弃用,共 10 个结点
    int n = 10;
    cout << "i   层号  双亲  左孩子  右孩子  是否叶子" << endl;
    for (int i = 1; i <= n; i++) {
        int depth = (int)log2(i) + 1;       // 第几层
        int p  = i / 2;                       // 双亲 = ⌊i/2⌋
        int lc = 2 * i;                       // 左孩子 = 2i
        int rc = 2 * i + 1;                   // 右孩子 = 2i+1
        bool leaf = (lc > n);
        cout << i << "    " << depth << "    " << p << "    "
             << (lc <= n ? lc : 0) << "     "
             << (rc <= n ? rc : 0) << "     "
             << (leaf ? "是" : "否") << endl;
    }
    cout << "最后一个非叶子结点下标 = " << n / 2
         << "(即结点 " << a[n / 2] << ")" << endl;
    cout << "层序遍历 = 数组顺序:";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}
运行结果
i   层号  双亲  左孩子  右孩子  是否叶子
1    1    0    2     3     否
2    2    1    4     5     否
3    2    1    6     7     否
4    3    2    8     9     否
5    3    2    10     0     否
6    3    3    0     0     是
7    3    3    0     0     是
8    4    4    0     0     是
9    4    4    0     0     是
10    4    5    0     0     是
最后一个非叶子结点下标 = 5(即结点 5)
层序遍历 = 数组顺序:1 2 3 4 5 6 7 8 9 10 

规律一目了然:第 i 个结点的左孩子在 2i、右孩子在 2i+1、双亲在 ⌊i/2⌋。并且层序遍历 = 数组顺序,所以遍历完全二叉树只要一个 for 循环!

为什么普通二叉树不行普通二叉树有“缺孩子”,2i/2i+1 会对不上真实结构,只能链式存

③ 1-based 还是 0-based?

很多比赛题用 0-based(下标从 0 开始)。公式要改,但思想一样:

C++完全二叉树·数组存储(0-based)
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int n = 10; // 0-based 编号,共 10 个结点
    cout << "i   层号  双亲  左孩子  右孩子" << endl;
    for (int i = 0; i < n; i++) {
        int depth = (int)log2(i + 1) + 1;
        int p  = (i == 0) ? -1 : (i - 1) / 2; // 双亲 = ⌊(i-1)/2⌋
        int lc = 2 * i + 1;                    // 左孩子 = 2i+1
        int rc = 2 * i + 2;                    // 右孩子 = 2i+2
        cout << i << "    " << depth << "    " << p << "    "
             << (lc < n ? lc : -1) << "     "
             << (rc < n ? rc : -1) << endl;
    }
    return 0;
}
运行结果
i   层号  双亲  左孩子  右孩子
0    1    -1    1     2
1    2    0    3     4
2    2    0    5     6
3    3    1    7     8
4    3    1    9     -1
5    3    2    -1     -1
6    3    2    -1     -1
7    4    3    -1     -1
8    4    3    -1     -1
9    4    4    -1     -1
约定双亲左孩子右孩子
1-based(a[0]弃用)⌊i/2⌋2i2i+1
0-basedi=0 时无双亲;i>0 时 ⌊(i-1)/2⌋2i+12i+2

⚠️ 用哪种约定,前后必须一致。写题时先定好,再代入公式,别混用!

④ 满二叉树 与 完全二叉树 的关系

满二叉树:每一层的结点数都达到最大(第 k 层有 2k-1 个),即每个非叶子结点都有两个孩子。

  • 满二叉树 一定是完全二叉树(最后一层也从左连续、且无空位)。
  • 完全二叉树 不一定是满二叉树(最后一层可能没填满)。
  • 即:满二叉树 ⊂ 完全二叉树。满二叉树是完全二叉树的特例。
C++满二叉树(n=15)的深度与最后非叶子
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int n = 15;                                  // 满二叉树,15 个结点
    int k = (int)log2(n) + 1;                     // 深度 = ⌊log2 n⌋ + 1
    cout << "n = " << n
         << "  深度 = " << k
         << "  最后一个非叶子结点下标 = " << n / 2 << endl;
    return 0;
}
运行结果
n = 15  深度 = 4  最后一个非叶子结点下标 = 7

满二叉树 n 个结点,深度 k = ⌊log₂n⌋+1;最后一个非叶子结点下标 = ⌊n/2⌋。这两个公式对所有完全二叉树都成立。

⑤ 数组里的堆:大顶堆 / 小顶堆

“堆”就是一棵满足堆性质的完全二叉树:每个结点的值都 ≥(大顶堆)或 ≤(小顶堆)它的两个孩子。因为是完全二叉树,堆天生用数组存——这正是优先队列(priority_queue)的底层。

C++建大顶堆(向下调整 down)
#include <iostream>
using namespace std;
void down(int a[], int n, int i) {           // 大顶堆向下调整(1-based)
    int largest = i;
    int l = 2 * i, r = 2 * i + 1;
    if (l <= n && a[l] > a[largest]) largest = l;
    if (r <= n && a[r] > a[largest]) largest = r;
    if (largest != i) {
        int t = a[i]; a[i] = a[largest]; a[largest] = t; // 手动交换
        down(a, n, largest);
    }
}
int main() {
    int a[] = {0, 4, 10, 3, 5, 1, 9, 7, 6, 8, 2}; // 1-based,n = 10
    int n = 10;
    cout << "建堆前:";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    for (int i = n / 2; i >= 1; i--) down(a, n, i); // 从最后一个非叶子 ⌊n/2⌋ 起
    cout << "建堆后(大顶堆):";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}
运行结果
建堆前:4 10 3 5 1 9 7 6 8 2 
建堆后(大顶堆):10 8 9 6 2 3 7 4 5 1 

关键事实:最后一个非叶子结点下标 = ⌊n/2⌋。建堆时从 ⌊n/2⌋ 往前,对每个结点做“向下调整”,就能把任意数组变成大顶堆。向下调整的核心:比较自己与左右孩子,若不如最大的孩子大,就交换,再继续往下。

📌 堆排序、优先队列都以“完全二叉树 + 数组”为基础;完整堆排序属拓展内容(见模块四)。

03

实践应用

三道分析编程题:判断完全二叉树、求结点所在层与祖先、建大顶堆。均给出可编译运行的参考实现。

🧩 题1:输入层序序列,判断是否完全二叉树

给定层序序列(# 表示空),判断它能否构成完全二叉树。核心:一旦遇到空结点,后面就不能再出现非空结点。

C++判断是否完全二叉树
#include <iostream>
#include <string>
#include <queue>
using namespace std;
struct Node { int val; Node *l, *r; Node(int v) : val(v), l(nullptr), r(nullptr) {} };
// 按层序序列(# 表示空)建树,判断是否为完全二叉树
bool isComplete(const string s[], int n) {
    queue<Node*> q;
    q.push(new Node(stoi(s[0])));
    int idx = 1;
    bool seenNull = false;
    while (!q.empty()) {
        Node* cur = q.front(); q.pop();
        if (idx < n) {                          // 左孩子
            if (s[idx] == "#") { cur->l = nullptr; seenNull = true; }
            else { cur->l = new Node(stoi(s[idx])); q.push(cur->l); if (seenNull) return false; }
            idx++;
        }
        if (idx < n) {                          // 右孩子
            if (s[idx] == "#") { cur->r = nullptr; seenNull = true; }
            else { cur->r = new Node(stoi(s[idx])); q.push(cur->r); if (seenNull) return false; }
            idx++;
        }
    }
    return true;
}
int main() {
    string s1[] = {"1","2","3","4","5","6","7","8","9","10"};
    int n1 = sizeof(s1) / sizeof(s1[0]);
    cout << "示例1 层序(1..10):是否完全二叉树 → "
         << (isComplete(s1, n1) ? "是" : "否") << endl;

string s2[] = {"1","2","3","#","#","6","7"};
    int n2 = sizeof(s2) / sizeof(s2[0]);
    cout << "示例2 层序(1 2 3 # # 6 7):是否完全二叉树 → "
         << (isComplete(s2, n2) ? "是" : "否") << endl;
    return 0;
}
运行结果
示例1 层序(1..10):是否完全二叉树 → 是
示例2 层序(1 2 3 # # 6 7):是否完全二叉树 → 否

思路:用队列按层序建树,设标志 seenNull。遇到空孩子就置位;之后再遇到非空孩子直接判否。

🧩 题2:求结点所在层与祖先链

完全二叉树用数组存,求某结点 i 在第几层、以及从根到它的祖先链。层号 = ⌊log₂i⌋+1;祖先链反复 i = ⌊i/2⌋ 直到 0。

C++完全二叉树:层、最后非叶子、关系表
#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int a[] = {0, 1, 2, 3, 4, 5, 6, 7}; // 1-based,共 7 个结点(满二叉树)
    int n = 7;
    int k = (int)log2(n) + 1;
    cout << "层序遍历(= 数组顺序):";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    cout << "深度 = " << k
         << "  最后一个非叶子结点下标 = " << n / 2
         << "(结点 " << a[n / 2] << ")" << endl;
    cout << "i   双亲  左孩子  右孩子" << endl;
    for (int i = 1; i <= n; i++) {
        int p = i / 2, lc = 2 * i, rc = 2 * i + 1;
        cout << i << "    " << p << "    "
             << (lc <= n ? lc : 0) << "     "
             << (rc <= n ? rc : 0) << endl;
    }
    return 0;
}
运行结果
层序遍历(= 数组顺序):1 2 3 4 5 6 7 
深度 = 3  最后一个非叶子结点下标 = 3(结点 3)
i   双亲  左孩子  右孩子
1    0    2     3
2    1    4     5
3    1    6     7
4    2    0     0
5    2    0     0
6    3    0     0
7    3    0     0

注意层序遍历直接等于数组顺序,所以“输出整棵树”就是一个普通 for 循环——这是数组表示法最大的便利。

🧩 题3:建大顶堆(向下调整)

给定数组,从最后一个非叶子结点 ⌊n/2⌋ 起向前,对每个结点调用 down 向下调整,把数组整理成大顶堆。

C++向下调整建堆(同卡片⑤)
#include <iostream>
using namespace std;
void down(int a[], int n, int i) {           // 大顶堆向下调整(1-based)
    int largest = i;
    int l = 2 * i, r = 2 * i + 1;
    if (l <= n && a[l] > a[largest]) largest = l;
    if (r <= n && a[r] > a[largest]) largest = r;
    if (largest != i) {
        int t = a[i]; a[i] = a[largest]; a[largest] = t; // 手动交换
        down(a, n, largest);
    }
}
int main() {
    int a[] = {0, 4, 10, 3, 5, 1, 9, 7, 6, 8, 2}; // 1-based,n = 10
    int n = 10;
    cout << "建堆前:";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    for (int i = n / 2; i >= 1; i--) down(a, n, i); // 从最后一个非叶子 ⌊n/2⌋ 起
    cout << "建堆后(大顶堆):";
    for (int i = 1; i <= n; i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}
运行结果
建堆前:4 10 3 5 1 9 7 6 8 2 
建堆后(大顶堆):10 8 9 6 2 3 7 4 5 1 

要点:down(a,n,i) 只负责“以 i 为根的子树”满足大顶堆;从 ⌊n/2⌋ 到 1 依次调用,整棵树就满足堆性质。时间复杂度 O(n)。

04

总结提升

把核心公式、易混概念、拓展方向一次性理清。

📌 核心要点总结

结论公式 / 说明
完全二叉树定义前 k-1 层满,第 k 层左连续无空位
1-based 双亲 / 左 / 右⌊i/2⌋ / 2i / 2i+1
0-based 双亲 / 左 / 右i>0 时 ⌊(i-1)/2⌋ / 2i+1 / 2i+2(i=0 无双亲)
深度 kk = ⌊log₂n⌋ + 1
最后非叶子结点下标⌊n/2⌋
满 ⊂ 完全满二叉树是完全二叉树的特例
层序遍历= 数组顺序,for 循环即可
堆满足堆性质的完全二叉树;大顶堆父 ≥ 子

🚀 拓展:提高内容(CSP-J 不要求,了解即可)

  • 堆排序:建大顶堆后,反复把堆顶(最大值)换到末尾并缩小堆、向下调整,得到升序序列,时间 O(n log n)。
  • 优先队列:C++ priority_queue 底层就是堆(默认大顶堆);小顶堆用 greater<int>。
  • 线段树 / 二叉堆数组:很多“区间查询”“动态最值”结构都建立在“完全二叉树用数组存”这一思想上。

⚠️ 以上属提高/拓展,CSP-J 初赛与复赛均不要求会写堆排序,但理解“完全二叉树可用数组存”对后续学习非常关键。

💡 思考与启发

  • 为什么“完全”这个条件这么重要?换成普通二叉树,2i/2i+1 会错在哪?
  • 0-based 与 1-based 只是“偏移 1”,但为什么很多人会在边界(i=0 的双亲)写错?怎样避免?
  • 堆能只用数组存,是因为它“完全”;如果堆不是完全二叉树,还能只用数组吗?
05

学习检测

5 大易错点 + 速查表 + 笔记卡,考前一眼回顾。

⚠️ 5 大易错点

易错1

把“完全二叉树”记成“满二叉树”。满 ⊂ 完全,完全二叉树最后一层可以没填满,只要左连续。

易错2

孩子公式用错:写成 2i+1、2i+2(那是 0-based)。1-based 是 2i、2i+1,别混。

易错3

⌊i/2⌋ 忘记向下取整,或 0-based 时 i=0 还去求双亲导致越界。先判断 i 是否为根。

易错4

判断完全二叉树只看“结点数”,不看“位置”。关键是层序遇空后再有非空即否。

易错5

建堆从 1 或 n 开始向下调整。正确起点是最后一个非叶子结点 ⌊n/2⌋,向前到 1。

📑 速查表

需求1-based0-based
双亲⌊i/2⌋i>0:⌊(i-1)/2⌋;i=0 无
左孩子2i2i+1
右孩子2i+12i+2
最后非叶子⌊n/2⌋⌊(n-2)/2⌋(即 ⌊n/2⌋-1)
深度⌊log₂n⌋+1

📌 笔记卡

完全二叉树 = 数组存储的最爱

① 定义:前 k-1 层满,第 k 层左连续无空位。

② 公式(1-based):双亲 ⌊i/2⌋,左 2i,右 2i+1。

③ 层序遍历 = 数组顺序。

④ 满二叉树 ⊂ 完全二叉树。

⑤ 堆 = 满足堆性质的完全二叉树,最后非叶子 = ⌊n/2⌋。

课后练习

分层练习作业

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

🟢 A组 · 基础(4题)

A1基础·判断

满二叉树一定是完全二叉树吗?完全二叉树一定是满二叉树吗?各举一个例子说明。

画图说明两种情形:满二叉树(每层都满)必然也是完全二叉树;完全二叉树(最后一层没填满)通常不是满二叉树。

查看参考答案

满二叉树一定是完全二叉树(✔);完全二叉树不一定是满二叉树(✘)。例如 n=3 的完全二叉树(根+左右孩子,最后一层未满)是完全但不是满。

考点:满二叉树每层都填满,必然满足完全二叉树“从上到下、从左到右连续排列”的要求;反过来结点数没凑满 2^k-1 时,完全但不满。

易错:把二者当成等价概念,忽略 n=3 这种“是完全但不满”的情形。

A2基础·公式

含 n 个结点的完全二叉树用 1-based 数组存。写出结点 i(i>1)的双亲、左孩子、右孩子下标。

直接套公式即可。

查看参考答案

双亲 = ⌊i/2⌋,左孩子 = 2i,右孩子 = 2i+1(若超出 n 则不存在)。

思路:1-based 编号下父子关系直接由下标给出:双亲 = ⌊i/2⌋,左孩子 = 2i,右孩子 = 2i+1,孩子下标超过 n 就说明不存在。

易错:把左右孩子记成 2i-1、2i,或忘记检查 2i、2i+1 是否越界(> n)。

A3基础·数组

7 个结点的满二叉树按层序存入 a[1..7](编号=值),写出 a[1]..a[7] 并标出每个结点的双亲。

层序编号 1..7 本身就是值。

查看参考答案

a=[_,1,2,3,4,5,6,7];双亲:2,3 的双亲是 1;4,5 的双亲是 2;6,7 的双亲是 3。

思路:满二叉树按层序编号后正好填满 a[1..7],对每个非根结点 i,它的双亲就是 ⌊i/2⌋。

易错:误用 0-based 下标去套公式,双亲结果全部偏一位。

A4基础·计算

n=15 的完全二叉树深度是多少?最后一个非叶子结点下标是多少?(用公式)

套 k=⌊log₂n⌋+1 与 ⌊n/2⌋。

查看参考答案

k=⌊log₂15⌋+1=3+1=4;最后非叶子=⌊15/2⌋=7。

思路:深度 k = ⌊log2 15⌋ + 1 = 4;最后一个非叶子结点满足 2i ≤ n,即 i = ⌊n/2⌋ = 7。

易错:把“最后一个非叶子结点”当成最后一个结点 15,或求深度时忘记加 1。

🟡 B组 · 提高(4题)

B1提高·0-based

0-based 数组存完全二叉树,结点 i 的双亲 / 左 / 右下标公式?i=0 时双亲是什么?

对比 1-based 推导:根编号为 0,没双亲。

查看参考答案

i>0 时双亲 = ⌊(i-1)/2⌋,左 = 2i+1,右 = 2i+2;i=0 是根,没有双亲(代码中要先判断避免越界)。

思路:0-based 下标相当于把 1-based 整体平移 1:左孩子 = 2i+1,右孩子 = 2i+2,双亲 = ⌊(i-1)/2⌋;i = 0 是根,没有双亲。

易错:照搬 1-based 的 2i、2i+1,或对 i = 0 直接计算 ⌊(0-1)/2⌋ 造成负数越界。

B2提高·判断

层序序列 1 2 3 # # 6 7,判断是否完全二叉树?说明原因。

按层序编号后看“空位之后是否还有非空”。

查看参考答案

不是。编号 4、5 是空的,但编号 6、7(结点 6、7)非空,出现“空位后有非空”,违反完全二叉树定义。

考点:完全二叉树的层序编号中间不能出现“有结点—空位—又有结点”。这里 4、5 为空而 6、7 非空,编号不连续,故不是完全二叉树。

易错:只看“每个结点至多两个孩子”就判成完全二叉树,忘了编号连续这一要求。

B3提高·查表

a[1..10]={0,1,2,3,4,5,6,7,8,9,10}。结点 5 的左孩子是几?它在第几层?

用 2i 与 ⌊log₂i⌋+1。

查看参考答案

左孩子 = 2×5 = 10(存在);层号 = ⌊log₂5⌋+1 = 2+1 = 3 层。

思路:结点 5 的左孩子 = 2×5 = 10,10 ≤ n 故存在;它的层号 = ⌊log2 5⌋ + 1 = 3。

易错:把层号算成 ⌊log2 5⌋ = 2,漏掉公式里的 +1。

B4提高·思路

用文字 + 伪代码说明大顶堆向下调整 down(a,n,i) 的核心思路(不要求完整 C++)。

比较自己与左右孩子,不够大就下沉。

查看参考答案

取 i 与左右孩子中值最大者;若最大者不是 i,交换并使 i=最大者,递归对原最大者位置继续 down,直到 i 最大或越界。

思路:down 从结点 i 出发,比较它与左右孩子哪个更大;若最大者不是 i 就交换,并让 i 移到原最大者的位置继续向下调整,直到 i 本身最大或孩子不存在。

易错:只和其中一个孩子比较(漏掉另一个),或交换一次就收手,没有把影响继续往下传递。

🔴 C组 · 挑战(4题)

C1挑战·证明

证明:含 n 个结点的完全二叉树,最后一个非叶子结点下标 = ⌊n/2⌋。

从“最后一个结点 n 的双亲是 ⌊n/2⌋”切入。

查看参考答案

结点 n 的双亲是 ⌊n/2⌋。由于完全二叉树结点编号连续,n 的双亲是“最后一个有孩子的结点”;再往后编号 >⌊n/2⌋ 的结点都没有孩子(是叶子)。故最后非叶子 = ⌊n/2⌋。

思路:结点 n 的双亲是 ⌊n/2⌋;所有编号大于 ⌊n/2⌋ 的结点都满足 2i > n、没有孩子,是叶子。因此编号最大的有孩子结点就是 ⌊n/2⌋。

易错:混淆“有孩子的结点”与“叶子”的编号范围,把最后一个非叶子结点说成 n。

C2挑战·推导

说明完全二叉树结点数 n 与深度 k 满足 2^(k-1) ≤ n ≤ 2^k - 1;并说明满二叉树 n=2^k-1 时取等号。

用各层最大结点数求和。

查看参考答案

前 k-1 层最多 2^(k-1)-1 个,加上第 k 层至少 1 个、最多 2^(k-1) 个,合计 2^(k-1) ≤ n ≤ 2^k-1。满二叉树第 k 层也满,n=2^k-1,取上界。

思路:前 k-1 层最多 2^(k-1) - 1 个结点;第 k 层最少 1 个、最多 2^(k-1) 个,两段相加即得 2^(k-1) ≤ n ≤ 2^k - 1。满二叉树第 k 层也是满的,故取到上界。

易错:把第 k 层的最大结点数写成 2^k(应是 2^(k-1)),导致上界偏大。

C3挑战·编码

手写 C++:输入层序序列(# 表示空),输出是否完全二叉树,并输出 1-based 数组存储。

复用判断是否完全的算法,同时收集非空值到数组。

查看参考答案

按层序读入,遇空标记 seenNull;非空结点依次存入 a[++cnt],若 seenNull 已为真则输出“否”。最后输出 a[1..cnt]。

思路:按层序读入,用 seenNull 记录“此前是否已出现过空位”:读到 # 就置真,之后若再读到非 # 就说明编号不连续,输出“否”;非空结点顺序存入 a[++cnt]。

易错:只在“空位的孩子位置出现非空”时判否,漏掉了“空位之后又冒出别的非空结点”这一更一般的情形。

C4挑战·编码

手写 C++:把给定数组建为大顶堆(从 ⌊n/2⌋ 起向下调整),输出建堆后数组。

实现 down 函数,循环 i=⌊n/2⌋..1。

查看参考答案

实现 down(a,n,i):比较 a[i] 与左右孩子,必要时交换并递归;主程序 for(i=n/2;i>=1;i--) down(a,n,i);输出 a[1..n]。

思路:从最后一个非叶子结点 ⌊n/2⌋ 开始倒序到 1,对每个 i 做一次 down;down 内部比较 a[i] 与左右孩子,若孩子更大就交换并继续向下调整。

易错:从 1 开始正向调整(应从 ⌊n/2⌋ 倒序),或忘记只有 2i ≤ n 时才存在孩子可比较。

🏆 洛谷原题(20道)

1P1090合并果子(优先队列 / 小根堆)普及-
2P5836[USACO19DEC] Milk Visits S普及-
3P1087FBI树(完全二叉树 · 数组表示)普及-
4P4913二叉树深度(数组完全二叉树)入门
5P1305新二叉树普及-
6P1030求先序排列普及-
7P1827美国血统普及-
8P1229遍历问题提高-
9P2015二叉苹果树提高-
10P3884满二叉树的带权路径长度普及-
11P5018对称二叉树普及-
12P1364医院设置提高-
13P1395会议(树的重心)提高-
14P1040加分二叉树提高-
15P3379最近公共祖先 LCA(模板)提高-
16P2052道路铺设提高-
17P1268树的最大独立集提高-
18P2014选课(树形 DP)提高-
19P5536【XR-3】核心城市提高-
20P1198[JSOI2008] 最大数(线段树 · 数组)提高-