目录
-
- 引言
- [核心板块一:物理映射法则------顺序 vs 链式的终极抉择](#核心板块一:物理映射法则——顺序 vs 链式的终极抉择)
-
- [1. 顺序存储的数学基石](#1. 顺序存储的数学基石)
- [2. 链式存储的指针网络](#2. 链式存储的指针网络)
- 核心板块二:顺序存储的巅峰------堆(Heap)
-
- [1. 核心原子操作](#1. 核心原子操作)
- [2. 堆的工程构建](#2. 堆的工程构建)
- [3. 堆的动态增删 (Push & Pop)](#3. 堆的动态增删 (Push & Pop))
- 核心板块三:堆的工业级应用
-
- [1. 原地堆排序](#1. 原地堆排序)
- [2. 海量数据 Top-K 问题](#2. 海量数据 Top-K 问题)
- 核心板块四:链式二叉树的遍历全解
-
- [1. 递归深度优先遍历(前、中、后序)](#1. 递归深度优先遍历(前、中、后序))
- [2. 迭代遍历(非递归)](#2. 迭代遍历(非递归))
- [3. 广度优先遍历------层序遍历](#3. 广度优先遍历——层序遍历)
- 核心板块五:分治思维与基建操作
- 核心板块六:二叉树的重构与销毁
-
- [1. 前序序列化构建](#1. 前序序列化构建)
- [2. 前中/后中序列唯一确定二叉树](#2. 前中/后中序列唯一确定二叉树)
- [3. 内存安全回收(后序销毁)](#3. 内存安全回收(后序销毁))
- 核心板块七:经典OJ题思路通关
-
- [1. 结构判断类](#1. 结构判断类)
- [2. 结构嵌套类------子树匹配(https://leetcode.cn/problems/univalued-binary-tree/description/)](#2. 结构嵌套类——子树匹配)
- [3. 路径追踪类------最近公共祖先(https://leetcode.cn/problems/univalued-binary-tree/description/)](#3. 路径追踪类——最近公共祖先)
- 完全二叉树的判定
- 总结与预告
- 附录:完整源码
-
- [1. BinaryTree.hpp](#1. BinaryTree.hpp)
- [2. Queue.hpp](#2. Queue.hpp)
- [3. Heap.hpp](#3. Heap.hpp)
引言
数据结构的学习中,树的概念与性质是前奏,真正的考验在于用代码将树"种"进内存,并让它可遍历、可查找、可销毁。如果说上一篇《图解树与二叉树:从底层逻辑到数学性质的深度重构》为你描摹了骨骼框架,那么本文就是往骨架上填充肌肉与神经:我们将直接用C语言从头实现二叉树的物理存储、四种遍历、属性计算、重构与销毁,并以完整的堆(Heap)代码为基础,解析建堆、堆排序和Top-K问题。凡是代码片段,皆取自经过完整测试的工程文件;凡是概念推导,必配合底层调用栈逻辑。阅读完本文,你将真正获得"徒手撕二叉树与堆"的能力。
核心板块一:物理映射法则------顺序 vs 链式的终极抉择
1. 顺序存储的数学基石
完全二叉树可以使用一维数组直接映射:对于下标从0开始的数组,若父结点下标为 i,则左孩子为 2*i+1,右孩子为 2*i+2;反之孩子下标 i 的父结点为 (i-1)/2(整除)。这套公式允许 O(1) 时间找到父子关系,是堆和线段树的物理基础。
然而,普通二叉树若强行放入数组,会发现大量单元空置。例如一棵右斜树,第 k 层只有一个结点,但数组却要为它分配 2^(k-1) 个位置的空间,绝大部分被空值占据,形成"内存黑洞"。因此对非完全二叉树,顺序存储不可接受。

2. 链式存储的指针网络
二叉链表是链表思想在树上的延伸:每个结点包含一个数据域和两个指针域,分别指向左孩子和右孩子。我们使用如下结构体定义(完整代码见附录):
c
typedef int BTDataType;
typedef struct BinaryTreeNode {
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
} BTNode;
证明:n个结点的二叉链表必有 n+1 个空闲指针域。
证明:结点总数 n,已知二叉链表共 2n 个指针域。除根结点以外,每个结点恰有一条来自双亲的"连线"(即被一个指针指向),故非空指针域的数量为 n-1。于是空指针域数量 = 2n - (n-1) = n+1。这 n+1 个空闲指针域正是后续线索化二叉树和 Morris 遍历的空间来源。
核心板块二:顺序存储的巅峰------堆(Heap)
堆是一种特殊的完全二叉树,其物理结构采用数组存储,逻辑上要求任意结点的值不小于(或不大于)其孩子。大根堆:父 ≥ 子;小根堆:父 ≤ 子。
堆的结构体封装了动态数组、当前元素个数和容量:
c
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
} HP;
1. 核心原子操作
向上调整 (AdjustUp):新元素从尾部插入后,不断与父结点比较,若违反堆秩序则交换,直至满足堆定义。
c
void AdjustUp(HPDataType* a, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] < a[parent]) { // 小根堆:子小于父则上浮
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
} else break;
}
}
向下调整 (AdjustDown):前提是该结点的左右子树均已满足堆的性质。从父结点开始,选择两个孩子中较小(小根堆)者进行比较,若违反则交换并继续向下。该操作时间复杂度 O(log n)。
c
void AdjustDown(HPDataType* a, int n, int parent) {
int child = parent * 2 + 1; // 先假定左孩子
while (child < n) {
// 右孩子存在且更小,则选择右孩子
if (child + 1 < n && a[child] > a[child + 1]) {
++child;
}
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
} else break;
}
}
2. 堆的工程构建
从最后一个非叶子节点开始(下标为 (n-1-1)/2),自底向上对每个结点做一次向下调整。我们提供两种方式:通过逐个插入元素并向上调整,或者直接将随机数组堆化。HeapInitArray 函数即采用后向遍历的堆化策略:
c
void HeapInitArray(HP* php, int* a, int n) {
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = n;
php->capacity = n;
// 从倒数第一个非叶子结点开始向下调整建堆,时间复杂度O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(php->a, n, i);
}
}
时间复杂度严格推导:利用错位相减法可证,自底向上建堆的时间复杂度为 O(N),而非直觉中的 O(N log N)。因为越下层结点数越多,但每个结点向下调整的高度却越小,总和收敛于线性。
3. 堆的动态增删 (Push & Pop)
- 插入 (HeapPush):在数组尾部放入新元素,然后向上调整。
c
void HeapPush(HP* php, HPDataType x) {
// 扩容逻辑略
php->a[php->size] = x;
AdjustUp(php->a, ++php->size - 1);
}
- 删除堆顶 (HeapPop):将堆顶与最后一个元素交换,缩小堆大小,然后对新的堆顶做一次向下调整。
c
void HeapPop(HP* php) {
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
AdjustDown(php->a, --php->size, 0);
}
获取堆顶元素、判空等辅助函数同样简洁,完整实现参阅附录。
核心板块三:堆的工业级应用
1. 原地堆排序
利用大根堆实现升序排序(反之用小根堆实现降序)。步骤:先将无序数组建为大根堆;然后循环将堆顶(当前最大值)与尾部元素交换,并对剩余部分向下调整,重复直到堆尺寸为1。此过程完全在原数组上完成,空间复杂度 O(1),平均时间复杂度 O(N log N)。
c
//升序建大堆,降序建小堆,时间复杂度O(NlogN)
void HeapSort(int* a, int n)
{
//直接用a数组进行建堆O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; i--){
AdjustDown(a, n, i);
}
//O(NlogN)
int end = n - 1; //下标
while (end > 0){
Swap(&a[0], &a[end]);
AdjustDown(a, end--, 0);
}
}
2. 海量数据 Top-K 问题
场景:从 100 亿个数据中找出最大的前 100 个。核心解法:维护一个容量为 K 的小根堆。先读入前 K 个数建堆。之后每读入一个数,若它大于堆顶(当前第K大),则替换堆顶并向下调整。最终堆中就是最大的 K 个数。时间复杂度 O(N log K),空间仅需 K 个元素的数组。
c
void topk()
{
printf("请输入k:>");
int k = 0;
scanf("%d", &k);
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL){
perror("fopen error");
return;
}
int val = 0;
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL){
perror("malloc error");
return;
}
for (int i = 0; i < k; i++){
fscanf(fout, "%d", &minheap[i]);
}
// 建k个数据的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--){
AdjustDown(minheap, k, i);
}
int x = 0;
while (fscanf(fout, "%d", &x) != EOF){
// 读取剩余数据,比堆顶的值大,就替换他进堆
if (x > minheap[0]){
minheap[0] = x;
AdjustDown(minheap, k, 0);
}
}
for (int i = 0; i < k; i++){
printf("%d ", minheap[i]);
}
fclose(fout);
}
核心板块四:链式二叉树的遍历全解
遍历是二叉树一切操作的基础。下面的遍历函数全部提取自附录 BinaryTree.hpp,使用硬编码的示例树:
1
/ \
2 3
\ / \
4 5 6
1. 递归深度优先遍历(前、中、后序)
前序遍历 (根左右):
c
void PreOrder(BTNode* root) {
if (root) {
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
}
中序和后序只需调整 printf 与递归调用的顺序。
中序遍历 (左根右):
c
void InOrder(BTNode* root)
{
if (root)
{
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
}
后序遍历 (左右根):
c
void PostOrder(BTNode* root)
{
if (root)
{
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
}
递归的核心是利用函数调用栈自动保存现场:每进入一层递归,参数 root、局部变量和返回地址被压入系统栈;退出时弹出恢复,自然实现了"回退"的路径。
2. 迭代遍历(非递归)
思想是手动维护一个栈,沿着左子树一路到底,过程中压栈;遇空后弹出栈顶访问,再将当前指针转向右子树。后序非递归稍复杂,需要记录上一次访问的结点以避免重复进入右子树。面试中的高频考点。
c
// 简易数组栈(实际工程中应使用动态扩容栈)
#define MAX_STACK_SIZE 1000
typedef struct Stack {
BTNode* data[MAX_STACK_SIZE];
int top;
} Stack;
void StackInit(Stack* ps) { ps->top = 0; }
void StackPush(Stack* ps, BTNode* node) { ps->data[ps->top++] = node; }
BTNode* StackPop(Stack* ps) { return ps->data[--ps->top]; }
BTNode* StackTop(Stack* ps) { return ps->data[ps->top - 1]; }
int StackEmpty(Stack* ps) { return ps->top == 0; }
前序遍历: 栈是先进后出(LIFO),所以我们要先压入右孩子,再压入左孩子,这样出栈时就是先左后右。
c
void PreOrderIterative(BTNode* root) {
if (root == NULL) return;
Stack st;
StackInit(&st);
StackPush(&st, root);
while (!StackEmpty(&st)) {
BTNode* node = StackPop(&st);
printf("%d ", node->data); // 访问根
// 先压右孩子,后压左孩子
if (node->right) StackPush(&st, node->right);
if (node->left) StackPush(&st, node->left);
}
}
中序遍历:顺着左子树一路压栈直到尽头;遇空后弹出栈顶访问,然后将指针转向弹出节点的右子树。
c
void InOrderIterative(BTNode* root) {
Stack st;
StackInit(&st);
BTNode* cur = root;
while (cur != NULL || !StackEmpty(&st)) {
// 1. 一路向左,将左侧路径上的节点全部压栈
while (cur != NULL) {
StackPush(&st, cur);
cur = cur->left;
}
// 2. 左边走到头了,弹出栈顶,访问并转向右子树
BTNode* node = StackPop(&st);
printf("%d ", node->data);
// 3. 转向右子树(如果有右子树,下一轮会继续把它的左边界压栈)
cur = node->right;
}
}
后序遍历: 最复杂的一种。由于根节点必须在右子树访问完之后才能出栈,我们需要维护一个 last_visited 指针,用来记录上一次被访问(输出)的节点,以避免重复陷入右子树造成死循环。
c
void PostOrderIterative(BTNode* root) {
Stack st;
StackInit(&st);
BTNode* cur = root;
BTNode* last_visited = NULL; // 记录上一次访问的节点
while (cur != NULL || !StackEmpty(&st)) {
// 1. 一路向左压栈
while (cur != NULL) {
StackPush(&st, cur);
cur = cur->left;
}
// 2. 取栈顶元素(但不弹出)
BTNode* top = StackTop(&st);
// 3. 判断右子树是否为空,或者右子树是否刚被访问过
if (top->right == NULL || top->right == last_visited) {
// 右子树已经处理完毕,可以安全访问当前根节点
printf("%d ", top->data);
StackPop(&st);
last_visited = top; // 更新记录
} else {
// 右子树还未处理,转向右子树
cur = top->right;
}
}
}
3. 广度优先遍历------层序遍历
借助队列实现逐层输出。这里使用我们编写的 Queue 队列库(底层为单链表,存储指向二叉树结点的指针):
c
void LevelOrder(BTNode* root) {
Queue q;
QueueInit(&q);
if(root) QueuePush(&q, root);
while (!QueueEmpty(&q)) {
BTNode* node = QueueFront(&q);
printf("%d ", node->data);
if (node->left) QueuePush(&q, node->left);
if (node->right) QueuePush(&q, node->right);
QueuePop(&q);
}
QueueDestroy(&q);
}
核心板块五:分治思维与基建操作
许多二叉树问题可以用分治递归解决:先处理左子树,再处理右子树,最后合并结果。
- 节点总数:
c
int BinaryTreeSize(BTNode* root) {
return root == NULL ? 0 : 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
- 叶子节点数:
c
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
- 第 K 层节点数:
c
int BinaryTreeLevelKSize(BTNode* root, int k) {
if (root == NULL) return 0;
if (k == 1) return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
- 查找值为 X 的节点:
c
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
if (root == NULL) return NULL;
if (root->data == x) return root;
BTNode* ret = BinaryTreeFind(root->left, x);
if (ret) return ret;
ret = BinaryTreeFind(root->right, x);
if (ret) return ret;
return NULL;
}
- 二叉树高度(递归):
c
// 二叉树的高度-递归
int BinaryTreeHeight(BTNode* root) {
if (root == NULL) return 0;
int leftHeight = BinaryTreeHeight(root->left);
int rightHeight = BinaryTreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
核心板块六:二叉树的重构与销毁
1. 前序序列化构建
给定一个带空节点标记 # 的前序遍历序列,可唯一还原一棵二叉树。实现需借助一个全局索引指针:
c
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi) {
if (a[*pi] == '#') {
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL) {
perror("malloc error");
exit(1);
}
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a, pi);
root->right = BinaryTreeCreate(a, pi);
return root;
}
2. 前中/后中序列唯一确定二叉树
重构依赖于:前序/后序提供根结点,中序划分左右子树。仅凭前序和后序无法确定唯一二叉树,因为无法区分左右子树的界限。
核心口诀:"前序找根,中序切分"
以 前序 [1, 2, 4, 3, 5, 6]、中序 [2, 4, 1, 5, 3, 6] 为例,重构只需三步:
-
第一步:确立主根与划分子树
- 前序找根 :前序首位是
1,故整棵树的主根为1。 - 中序切分 :在中序中找到
1,将数组一分为二:- 左侧截出
[2, 4](共2个节点) ➔ 左子树 的前序为[2, 4],中序为[2, 4]。 - 右侧截出
[5, 3, 6](共3个节点) ➔ 右子树 的前序为[3, 5, 6],中序为[5, 3, 6]。
- 左侧截出
第二步:分治构建左子树 (前序
[2, 4],中序[2, 4])- 找根 :前序首位是
2,左子树的根为2。 - 切分 :在中序中找
2,左侧为空(说明无左孩子),右侧剩[4]。 - 小结 :节点
2的右孩子是4。左子树拼装完毕。
第三步:分治构建右子树 (前序
[3, 5, 6],中序[5, 3, 6])-
找根 :前序首位是
3,右子树的根为3。 -
切分 :在中序中找
3,左侧截出[5],右侧截出[6]。 -
小结 :节点
3的左孩子是5,右孩子是6。右子树拼装完毕。1 / \ 2 3 \ / \ 4 5 6
- 前序找根 :前序首位是
3. 内存安全回收(后序销毁)
销毁二叉树必须采用后序遍历,即先删除左右子树,再释放根结点。否则若先释放根,左右子树的指针将丢失,造成内存泄漏。
c
void BinaryTreeDestory(BTNode* root) {
if (root == NULL) return;
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
核心板块七:经典OJ题思路通关
1. 结构判断类
- 相同的树:同步遍历两棵树,若当前结点值不等或空非空状态不一致,则返回假。递归检查左右子树。
- 对称二叉树:转换为判断左树的左子与右树的右子,以及左树的右子与右树的左子是否对称。
- 单值二叉树:检查根与左右孩子值是否相等,并递归子树。
2. 结构嵌套类------子树匹配
判断 subRoot 是不是 root 的子树,需要双重递归:先判断以当前结点为根的树与 subRoot 是否相同;若不同,再递归去 root 的左、右子树中寻找匹配。
3. 路径追踪类------最近公共祖先
使用后序遍历自底向上返回目标结点。若某结点左右子树各返回一个目标结点,则该结点就是 LCA;若只有一个孩子返回目标,则继续向上传递。
完全二叉树的判定
利用层序遍历特性:遇到一个空结点之后,如果后面还在队列中遇到非空结点,则不是完全二叉树。实现已在代码库中:
c
int BinaryTreeComplete(BTNode* root) {
Queue q;
QueueInit(&q);
if(root) QueuePush(&q, root);
//层序遍历遇到空节点时停止
while (!QueueEmpty(&q)) {
BTNode* node = QueueFront(&q);
QueuePop(&q);
if (node == NULL) break;
QueuePush(&q, node->left);
QueuePush(&q, node->right);
}
//已经遇到空节点了,队列中还有非空节点就不是完全二叉树
while (!QueueEmpty(&q)) {
BTNode* node = QueueFront(&q);
QueuePop(&q);
if (node) { QueueDestroy(&q); return false; }
}
QueueDestroy(&q);
return true;
}
总结与预告
从链式存储到堆的顺序映射,从递归遍历到尾递归与队列层序,本文将二叉树的C语言实现打通至每一行代码。递归的分治套路、完全二叉树的数组映射、n+1空闲指针的数学定律,构成了后续高级树结构的基石。当给二叉树加上"左小右大"的约束,二叉搜索树便将登场,进而引出自平衡二叉树的终极对决。届时,代码的肌肉记忆将会让你游刃有余。
附录:完整源码
1. BinaryTree.hpp
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include "Queue.hpp"
typedef int BTDataType;
typedef struct BinaryTreeNode {
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
//
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc error");
exit(1);
}
node->data = x;
node->left = node->right = NULL;
return node;
}
//构建一棵二叉树
// 1
// / \
// 2 3
// \ / \
// 4 5 6
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node3;
node2->right = node4;
node3->left = node5;
node3->right = node6;
return node1;
}
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root)
{
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
}
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root)
{
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root)
{
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
}
// 层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* node = QueueFront(&q);
printf("%d ", node->data);
if (node->left)
{
QueuePush(&q, node->left);
}
if (node->right)
{
QueuePush(&q, node->right);
}
QueuePop(&q);
}
QueueDestroy(&q);
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)return 0;
if (root->left == NULL && root->right == NULL)return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)return 0;
if (k == 1)return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)return NULL;
if (root->data == x) return root;
BTNode* ret = NULL;
ret = BinaryTreeFind(root->left, x);
if (ret)return ret;
ret = BinaryTreeFind(root->right, x);
if (ret)return ret;
return NULL;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc error");
exit(1);
}
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a, pi);
root->right = BinaryTreeCreate(a, pi);
return root;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
return;
//后序遍历销毁节点,就不用再保存前节点
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* node = QueueFront(&q);
QueuePop(&q);
if (node == NULL)break;
QueuePush(&q, node->left);
QueuePush(&q, node->right);
}
//已经遇到空节点了,队列中还有非空节点就不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* node = QueueFront(&q);
QueuePop(&q);
if (node)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
//二叉树的高度-递归
int BinaryTreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = BinaryTreeHeight(root->left);
int rightHeight = BinaryTreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
2. Queue.hpp
c
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef struct BinaryTreeNode BTNode;
typedef BTNode* QDataType;
typedef struct QueueNode
{
QDataType x;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->phead;
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
QueueNode* QueueBuyNode(QDataType x)
{
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (newNode == NULL)
{
printf("malloc error");
exit(1);
}
newNode->x = x;
newNode->next = NULL;
return newNode;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QueueNode* newnode = QueueBuyNode(x);
if (pq->ptail == NULL)
{
pq->ptail = pq->phead = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->size > 0);
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QueueNode* node = pq->phead;
pq->phead = node->next;
free(node);
node = NULL;
}
pq->size--;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->size > 0);
return pq->phead->x;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->size > 0);
return pq->ptail->x;
}
3. Heap.hpp
c
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
} HP;
void Swap(HPDataType* p1, HPDataType* p2) {
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
} else break;
}
}
void AdjustDown(HPDataType* a, int n, int parent) {
int child = parent * 2 + 1;
while (child < n) {
if (child + 1 < n && a[child] > a[child + 1]) ++child;
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
} else break;
}
}
void HeapInit(HP* php) {
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php) {
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapPrint(HP* php) {
for (int i = 0; i < php->size; i++) printf("%d ", php->a[i]);
printf("\n");
}
void HeapInitArray(HP* php, int* a, int n) {
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = n;
php->capacity = n;
// 从第一个非叶子结点开始向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
AdjustDown(php->a, n, i);
}
void HeapPush(HP* php, HPDataType x) {
if (php->capacity == php->size) {
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
AdjustUp(php->a, ++php->size - 1);
}
void HeapPop(HP* php) {
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
AdjustDown(php->a, --php->size, 0);
}
HPDataType HeapTop(HP* php) { return php->a[0]; }
bool HeapEmpty(HP* php) { return php->size == 0; }