目录
[二、堆的 C 语言实现:完整代码解析](#二、堆的 C 语言实现:完整代码解析)
[2.1 头文件:Heap.h(定义结构体 + 函数声明)](#2.1 头文件:Heap.h(定义结构体 + 函数声明))
[2.2 源文件:Heap.c(实现堆的所有核心操作)](#2.2 源文件:Heap.c(实现堆的所有核心操作))
[1. 基础辅助函数:Swap(交换两个元素)](#1. 基础辅助函数:Swap(交换两个元素))
[2. 堆的初始化与销毁:HPInit & HPDestroy](#2. 堆的初始化与销毁:HPInit & HPDestroy)
[3. 上浮调整:AdjustUP(插入数据的核心)](#3. 上浮调整:AdjustUP(插入数据的核心))
[4. 向堆中插入数据:HPPush](#4. 向堆中插入数据:HPPush)
[5. 下沉调整:AdjustDown(删除 / 建堆的核心)](#5. 下沉调整:AdjustDown(删除 / 建堆的核心))
[6. 从堆中删除数据:HPPop(仅删除堆顶)](#6. 从堆中删除数据:HPPop(仅删除堆顶))
[7. 堆的基础查询操作:HPTop & HPEmpty](#7. 堆的基础查询操作:HPTop & HPEmpty)
[2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现)](#2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现))
[1. 堆的基础功能测试:TestHeap1](#1. 堆的基础功能测试:TestHeap1)
[2. 堆排序实现:HeapSort + TestHeap2](#2. 堆排序实现:HeapSort + TestHeap2)
[三、小根堆转大根堆:只需修改 2 处代码](#三、小根堆转大根堆:只需修改 2 处代码)
堆(Heap)是数据结构中一种特殊的完全二叉树,它不仅能高效实现优先队列 ,还是堆排序的核心基础,在 TopK 问题、任务调度等场景中应用广泛。本文将从堆的基础概念出发,结合完整的 C 语言代码实现,一步步讲透堆的初始化、插入、删除、堆排序等核心操作,全程通俗易懂,新手也能轻松掌握
一、堆的基础概念:什么是堆?
堆是一颗完全二叉树 ,且满足堆的性质 :根据节点值的大小关系,堆分为两种类型,我们本文主要实现小根堆(文末会说明大根堆的修改方法):
- 小根堆 :树中每个父节点的值 ≤ 其左右孩子节点的值,堆顶(根节点)是整个堆的最小值;
- 大根堆 :树中每个父节点的值 ≥ 其左右孩子节点的值,堆顶(根节点)是整个堆的最大值。
堆的关键特性
- 堆的物理存储:实际开发中,堆并非用二叉链表存储,而是用数组 (顺序存储),利用完全二叉树的节点下标关系实现父子节点的快速访问:
- 对于数组中下标为
parent的父节点,左孩子下标 为2*parent+1,右孩子下标 为2*parent+2; - 对于数组中下标为
child的孩子节点,父节点下标 为(child-1)/2(整数除法,自动向下取整)。
- 对于数组中下标为
- 堆的核心操作:上浮调整(AdjustUP) 和下沉调整(AdjustDown),这两个操作是实现堆插入、删除、建堆的基础。
二、堆的 C 语言实现:完整代码解析
本文的堆实现基于小根堆 ,代码分为三个文件:Heap.h(头文件,声明结构体和函数)、Heap.c(源文件,实现堆的核心操作)、Test.c(测试文件,验证堆的功能和堆排序),所有代码可直接编译运行,注释清晰。
2.1 头文件:Heap.h(定义结构体 + 函数声明)
首先定义堆的结构体和核心操作的函数声明,约定堆的存储类型为int(可通过HPDataType灵活修改),堆的结构体包含三个核心成员:存储数据的数组a、堆中有效元素个数size、数组的容量capacity。
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 定义堆的数据类型,可按需修改(如char、double)
typedef int HPDataType;
// 堆的结构体定义:顺序存储(数组)实现完全二叉树
typedef struct Heap
{
HPDataType* a; // 存储堆数据的数组
int size; // 堆中有效元素的个数
int capacity; // 数组的容量(避免频繁扩容)
}HP;
// 堆的核心操作函数声明
void HPInit(HP* php); // 初始化堆
void HPDestroy(HP* php); // 销毁堆(释放内存)
void HPPush(HP* php, HPDataType data);// 向堆中插入数据
void AdjustUP(HPDataType* a, int child);// 上浮调整(插入用)
void Swap(HPDataType* p1, HPDataType* p2);// 交换两个元素(辅助函数)
void AdjustDown(HPDataType* a, int n, int parent);// 下沉调整(删除/建堆用)
void HPPop(HP* php); // 从堆中删除数据(仅删除堆顶)
HPDataType HPTop(HP* php); // 获取堆顶数据
bool HPEmpty(HP* php); // 判断堆是否为空
2.2 源文件:Heap.c(实现堆的所有核心操作)
这是堆实现的核心,包含初始化、销毁、插入、删除、上浮、下沉等所有操作,每个函数都做了严格的断言校验,避免空指针、越界等问题,同时实现了数组的自动扩容。
1. 基础辅助函数:Swap(交换两个元素)
堆的调整过程中需要频繁交换父子节点的值,封装成通用函数,提高代码复用性。
cpp
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType temp = *p1;
*p1 = *p2;
*p2 = temp;
}
2. 堆的初始化与销毁:HPInit & HPDestroy
- 初始化:将数组置空,有效元素个数和容量初始化为 0,避免野指针;
- 销毁:释放数组的动态内存,再将所有成员置空,防止内存泄漏。
cpp
// 初始化堆
void HPInit(HP* php)
{
assert(php!= NULL); // 断言:避免传入空指针
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
// 销毁堆
void HPDestroy(HP* php)
{
assert(php!= NULL);
free(php->a); // 释放动态分配的数组内存
// 置空,避免野指针
php->a = NULL;
php->size = 0;
php->capacity=0;
}
3. 上浮调整:AdjustUP(插入数据的核心)
作用 :向堆中插入新元素后,新元素放在数组末尾(完全二叉树的最后一个节点),此时可能破坏堆的性质,需要通过上浮调整,让新元素找到自己的正确位置,恢复堆的性质。核心逻辑(小根堆):
- 根据孩子节点下标计算父节点下标;
- 比较孩子节点和父节点的值,若孩子节点更小,交换两者;
- 将父节点作为新的孩子节点,重复上述步骤,直到孩子节点成为根节点(下标为 0),或满足堆的性质。
cpp
void AdjustUP(HPDataType* a, int child)
{
int parent = (child - 1) / 2; // 孩子找父节点的公式
while (child > 0) // child=0表示到根节点,停止调整
{
// 小根堆:孩子<父节点,交换(大根堆改为>即可)
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
// 更新孩子和父节点,继续向上调整
child = parent;
parent = (child - 1) / 2;
}
else
{
break; // 满足堆的性质,直接退出
}
}
}
4. 向堆中插入数据:HPPush
核心步骤:
- 检查容量:若有效元素个数等于容量,进行数组扩容(初始容量 4,后续翻倍);
- 插入元素:将新元素放在数组末尾(
php->a[php->size]),有效元素个数 + 1; - 上浮调整:调用
AdjustUP,让新元素找到正确位置,恢复堆的性质。
cpp
void HPPush(HP* php, HPDataType data)
{
assert(php!= NULL);
// 容量不足,扩容:初始4,后续翻倍
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4: php->capacity * 2;
HPDataType* temp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (temp == NULL) // 扩容失败,报错并退出
{
perror("realloc failed");
exit(1);
}
php->a = temp;
php->capacity = newcapacity;
}
// 插入新元素到数组末尾
php->a[php->size] = data;
php->size++;
// 上浮调整,恢复堆的性质
AdjustUP(php->a, php->size - 1);
}
5. 下沉调整:AdjustDown(删除 / 建堆的核心)
作用 :删除堆顶元素或建堆时,堆的性质被破坏,需要通过下沉调整,让根节点(或指定节点)向下移动,找到自己的正确位置,恢复堆的性质。核心逻辑(小根堆):
- 根据父节点下标计算左孩子下标(默认左孩子为更小的孩子);
- 找到左右孩子中更小的那个(右孩子存在时才比较);
- 比较父节点和最小孩子节点的值,若父节点更大,交换两者;
- 将最小孩子节点作为新的父节点,重复上述步骤,直到孩子节点超出堆的范围(到达叶子节点),或满足堆的性质。
cpp
void AdjustDown(HPDataType* a, int n, int parent)
{
assert(a != NULL);
// 父节点找左孩子的公式,默认左孩子更小
int child = 2 * parent + 1;
while (child < n) // child>=n表示到达叶子节点,停止调整
{
// 找到左右孩子中更小的那个(右孩子存在才比较)
if (child + 1 < n && a[child + 1] < a[child])
{
child++; // 右孩子更小,切换到右孩子
}
// 小根堆:孩子<父节点,交换(大根堆改为>即可)
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
// 更新父节点和孩子节点,继续向下调整
parent = child;
child = 2 * parent + 1;
}
else
{
break; // 满足堆的性质,直接退出
}
}
}
6. 从堆中删除数据:HPPop(仅删除堆顶)
堆的删除操作仅支持删除堆顶元素 (堆的核心特性,若删除任意节点,需额外处理),核心步骤:
- 交换堆顶和堆的最后一个元素(将堆顶元素移到数组末尾,方便删除);
- 有效元素个数 - 1(逻辑删除,无需真正修改数组,后续扩容 / 插入会覆盖);
- 下沉调整:对新的堆顶元素调用
AdjustDown,恢复堆的性质。
cpp
void HPPop(HP* php)
{
assert(php != NULL);
assert(php->size); // 断言:堆为空时不能删除
// 交换堆顶和最后一个元素
Swap(&php->a[0], &php->a[php->size - 1]);
// 逻辑删除最后一个元素(原堆顶)
php->size--;
// 下沉调整新的堆顶,恢复堆的性质
AdjustDown(php->a, php->size,0);
}
7. 堆的基础查询操作:HPTop & HPEmpty
HPTop:获取堆顶元素(小根堆为最小值,大根堆为最大值),需断言堆非空;HPEmpty:判断堆是否为空,通过有效元素个数size是否为 0 实现。
cpp
// 获取堆顶数据
HPDataType HPTop(HP* php)
{
assert(php != NULL);
assert(php->size > 0); // 堆为空,不能获取堆顶
return php->a[0];
}
// 判断堆是否为空
bool HPEmpty(HP* php)
{
assert(php != NULL);
return php->size == 0;
}
2.3 测试文件:Test.c(验证堆的功能 + 堆排序实现)
测试文件包含两个核心测试:TestHeap1验证堆的插入、删除、获取堆顶 等基础功能,TestHeap2实现堆排序并验证,主函数可直接调用测试。
1. 堆的基础功能测试:TestHeap1
向堆中插入 10 个随机整数,然后循环 5 次获取并删除堆顶元素,小根堆的堆顶始终是当前堆的最小值,因此输出结果为0 1 2 3 4。
cpp
#include "Heap.h"
// 测试堆的基础功能:插入、删除、获取堆顶
void TestHeap1()
{
int a[10] = { 5,3,8,1,6,7,2,4,9,0 };
HP hp;
HPInit(&hp);
// 向堆中插入所有元素
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
HPPush(&hp, a[i]);
}
// 循环5次:获取堆顶并删除
int k = 5;
while (!HPEmpty(&hp)&&k--)
{
printf("%d ", HPTop(&hp)); // 输出堆顶(最小值)
HPPop(&hp); // 删除堆顶
}
HPDestroy(&hp); // 销毁堆,释放内存
}
2. 堆排序实现:HeapSort + TestHeap2
堆排序 是堆的经典应用,时间复杂度为O(nlogn),空间复杂度为O(1)(原地排序),核心思路(基于小根堆实现升序排序,大根堆可实现降序排序):
- 建堆 :将无序数组构建为小根堆,建堆的时间复杂度为
O(n);- 建堆从最后一个非叶子节点 开始(下标为
(n-1-1)/2),从后往前依次调用AdjustDown; - 最后一个非叶子节点:完全二叉树中,最后一个节点的父节点就是最后一个非叶子节点。
- 建堆从最后一个非叶子节点 开始(下标为
- 排序 :循环将堆顶(最小值)与堆的最后一个元素交换,然后对新的堆顶进行下沉调整,直到堆的有效元素个数为 1;
- 交换堆顶和最后一个元素:将最小值放到数组末尾,成为有序部分;
- 下沉调整:缩小堆的范围(
end--),对新堆顶调整,恢复堆的性质。
cpp
// 堆排序:基于小根堆实现升序排序
void HeapSort(int *a, int n)
{
// 步骤1:建堆(从最后一个非叶子节点开始,从后往前下沉调整)
for (int i = (n-1-1)/2; i >=0; i--)
{
AdjustDown(a,n,i);
}
// 步骤2:排序
int end = n - 1; // end表示堆的最后一个元素下标
while (end > 0)
{
Swap(&a[0], &a[end]); // 交换堆顶和最后一个元素,最小值归位
AdjustDown(a,end,0 ); // 对新堆顶下沉调整,堆的范围缩小为[0, end-1]
--end; // 缩小堆的范围
}
}
// 测试堆排序
void TestHeap2()
{
int a[] = { 500,3,8,1,6,2,4 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
// 输出排序结果:1 2 3 4 6 8 500
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
}
// 主函数:调用测试
int main()
{
//TestHeap1(); // 测试堆的基础功能
TestHeap2(); // 测试堆排序
return 0;
}
三、小根堆转大根堆:只需修改 2 处代码
本文实现的是小根堆,若需要实现大根堆 (堆顶为最大值,可实现降序堆排序),只需修改AdjustUP和AdjustDown中的比较条件 ,将<改为>即可,其余代码完全不变:
上浮调整AdjustUP:
cpp
// 大根堆:孩子>父节点,交换
if (a[child] > a[parent])
下沉调整AdjustDown:
cpp
// 大根堆:找到左右孩子中更大的那个
if (child + 1 < n && a[child + 1] > a[child])
// 大根堆:孩子>父节点,交换
if (a[child] > a[parent])
修改后,TestHeap1的输出会变为9 8 7 6 5 (堆顶始终是最大值),堆排序HeapSort会实现降序排序。
四、堆的核心应用场景
堆作为高效的优先队列实现,在实际开发中应用广泛,主要包括:
- 堆排序 :时间复杂度
O(nlogn),原地排序,适用于大数据量的排序场景; - TopK 问题 :找一组数据中最大 / 最小的 k 个元素(如找销量前 10 的商品、分数最高的 5 个学生),用小根堆 / 大根堆实现,时间复杂度
O(nlogk),比直接排序更高效; - 任务调度:如操作系统的进程调度、任务队列的优先级执行,优先级高的任务放在堆顶,优先执行;
- 图论算法 :如 Dijkstra 最短路径算法、Prim 最小生成树算法,用堆优化后可将时间复杂度从
O(n²)降为O(nlogn)。
五、总结
- 堆是完全二叉树 ,分为小根堆和大根堆,物理上用数组存储,利用父子节点的下标公式实现快速访问;
- 堆的核心操作是上浮调整(AdjustUP)和下沉调整(AdjustDown),插入用上浮,删除 / 建堆用下沉;
- 堆的插入和删除操作时间复杂度均为
O(logn),建堆时间复杂度为O(n),堆排序时间复杂度为O(nlogn); - 小根堆和大根堆的转换仅需修改比较条件,代码复用性高;
- 堆的核心优势是高效获取最值(堆顶),广泛应用于 TopK、任务调度、堆排序等场景。