【数据结构】二叉树与堆:C语言底层实现与核心算法

目录

引言

数据结构的学习中,树的概念与性质是前奏,真正的考验在于用代码将树"种"进内存,并让它可遍历、可查找、可销毁。如果说上一篇《图解树与二叉树:从底层逻辑到数学性质的深度重构》为你描摹了骨骼框架,那么本文就是往骨架上填充肌肉与神经:我们将直接用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
    • 中序切分 :在中序中找到 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; }