目录
一、堆的概述
堆是一种满足特定条件的完全二叉树
分为两种:
大根堆:所有父节点都大于等于子节点
小根堆:所有父节点都小于等于子节点
大根堆的示例:
对应的数组表示:
index: 0 1 2 3 4 5 6
value: 50 30 45 20 25 40 35
在这个大根堆中:
- 根节点50是最大的元素
- 每个父节点的值都大于其子节点的值
小根堆的示例:
对应的数组表示:
index: 0 1 2 3 4 5 6
value: 10 15 12 25 20 18 16
在这个小根堆中:
- 根节点10是最小的元素
- 每个父节点的值都小于其子节点的值
完全二叉树的特性使得可以用数组来存储,对于索引i的节点:
- 左子节点的索引为:2i + 1
- 右子节点的索引为:2i + 2
- 父节点的索引为:(i - 1)/ 2
定义一个堆:
//底层是一个可变数组,顺序表
typedef int HpDataType;
typedef struct Heap{
HpDataType* arr;
size_t size;
size_t capacity;
}Hp;
二、堆的常见操作
方法名 | 描述 | 时间复杂度 |
---|---|---|
HeapPush(Hp* ph, HpDataType val) | 堆顶插入元素 | O(logN) |
HeapPop(Hp* ph) | 堆顶元素出堆 | O(logN) |
boolEmpty(Hp* ph) | 判断堆是否为空 | O(1) |
HeapSize(Hp* ph) | 堆里有效元素个数 | O(1) |
HeapTop(Hp* ph) | 查找堆顶元素 | O(1) |
三、堆的实现(以大根堆为例)
1.堆的保存与表示
堆是完全二叉树,利用数组存储,元素代表节点值,索引代表节点在二叉树中的位置,任意节点索引为i,可通过公式来实现寻找左右子节点和父节点
- 左子节点的索引为:2i + 1
- 右子节点的索引为:2i + 2
- 父节点的索引为:(i - 1)/ 2
2.访问堆顶元素
HpDataType HeapTop(hp* ph) {
assert(ph);
return ph->arr[0];
}
3.堆顶插入元素
我们首先将元素插入数组末尾,即堆底,插入的元素可能会大于父节点,这时候堆的结构已经被破坏,所以我们需要修复从插入节点到根节点路径上的各个节点,从底至顶开始逐一比较修复
原始堆的示意图:
对应的数组表示:
index: 0 1 2 3 4 5 6
value: 50 30 45 20 25 40 35
以下是向大根堆中插入元素60的过程,向上调整算法示意:
- 首先将新元素插入到数组末尾
index: 0 1 2 3 4 5 6 7
value: 50 30 45 20 25 40 35 60
2.将新插入的元素与其父节点比较,如果大于父节点则交换位置(向上调整)
index: 0 1 2 3 4 5 6 7
value: 50 30 45 60 25 40 35 20
3.继续向上调整,直到满足堆的性质
index: 0 1 2 3 4 5 6 7
value: 60 30 45 50 25 40 35 20
入堆的代码实现:
void HeapPush(hp* ph, HpDataType val) {
assert(ph);
//空间不够需要扩容
if (ph->size == ph->capacity) {
//原容量为零,先给一个HpDataType的大小,若不为零,采用两倍扩容比例
HpDataType newCapacity = ph->capacity == 0 ? sizeof(HpDataType) : ph->capacity * 2;
//向堆申请新的空间
HpDataType* tmp = (HpDataType*)realloc(ph->arr, sizeof(HpDataType) * newCapacity);
if (tmp == NULL) {
perror("realloc err!");
return;
}
//更新容量和空间
ph->capacity = newCapacity;
ph->arr = tmp;
}
//向堆底插入元素,堆的元素数量加一
ph->arr[ph->size++] = val;
//此时堆的结构可能被破坏,向上调整算法修复堆的结构
AdjustUp(ph->arr, ph->size - 1);
}
向上调整的代码实现:
//向上调整算法,参数数组,参数开始调整叶节点的索引
AdjustUp(HpDataType* arr, size_t child) {
//找父节点索引
size_t parent = (child - 1) / 2;
//最坏的情况,叶节点调整为根节点
while (child > 0) {
//父节点小于等于子节点,交换两节点的值
if (arr[parent] <= arr[child]) {
Swap(&arr[parent], &arr[child]);
//跟新子节点的索引,现在子节点变为新爹
child = parent;
//继续找爹作比较
child = (child - 1) / 2;
}
else {
break;
}
}
}
交换函数的代码实现:
void Swap(HpDataType* pa, HpDataType* pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
- 时间复杂度分析:O(logn)
4.堆顶元素出堆
堆顶元素是根节点,也是数组首元素,直接删除首元素,整个堆的结构被破坏,所有节点索引都会改变,我们尽可能的减少元素索引变动
我们可以采用如下步骤:
- 交换堆顶元素与堆底元素(交换根节点与最右叶节点)(这样避免了索引变动)
- 交换完成后,将堆底从列表中删除(注意,由于已经交换,因此实际上删除的是原来的堆顶元素)
- 从根节点开始,使用向下调整算法完善堆的结构
以下是堆顶元素出堆的过程。首先看原始的大根堆:
index: 0 1 2 3 4 5 6
value: 50 30 45 20 25 40 35
出堆过程如下,向下调整算法:
- 首先将堆顶元素删除,用最后一个元素替换堆顶
index: 0 1 2 3 4 5 6
value: 35 30 45 20 25 40 [50已删除]
2.将新的堆顶元素与其较大的子节点比较,如果小于子节点则交换位置(向下调整)
index: 0 1 2 3 4 5 6
value: 45 30 35 20 25 40 [50已删除]
3.继续向下调整,直到满足堆的性质,此时已经满足大根堆的性质,调整完成
堆顶元素删除代码实现:
void HeapPop(hp* ph) {
assert(ph);
//将堆底元素移到堆顶,堆顶元素值被覆盖
ph->arr[0] = ph->arr[ph->size - 1];
//删除堆底元素
ph->size--;
//堆的结构被破坏,向下调整算法修复堆的结构
AdjustDown(ph->arr, ph->size, 0);
}
向下调整算法代码实现:
//向下调整算法,参数:数组 数组元素个数 开始向下调整的父节点索引
AdjustDown(HpDataType* arr, size_t size, size_t parent) {
//找较大的子节点,假设左子节点较大
size_t child = 2 * parent + 1;
//child索引不断增大,但不会超过数组元素个数
while (child < size) {
//假设错误,右子节点更大,更新索引
if (child + 1 < size && arr[child] < arr[child + 1]) {
child++;
}
//如果父节点小于等于子节点,交换两节点的值
if (arr[parent] <= arr[child]) {
Swap(&arr[parent], &arr[child]);
//更新父节点的索引,现在变成儿子了
parent = child;
//继续找儿子比较
child = 2 * parent + 1;
}
else {
break;
}
}
}
- 时间复杂度分析:O(logn)
5.查找堆顶元素
HpDataType HeapTop(hp* ph) {
assert(ph);
return ph->arr[0];
}
6.判断堆是否为空
bool HeapEmpty(hp* ph) {
assert(ph);
return ph->size == 0;
}
四、堆的常见操作
1. 建堆操作
方式一、 自顶向下插入法(Top-down)
步骤:
- 初始化空堆。
- 逐个插入元素:每次将新元素添加到堆的末尾。
- 向上调整:对新插入的元素执行HeapPush 操作,使其满足堆性质。
插入顺序:4 → 10 → 3 → 5 → 1
步骤:
1. 插入4 → [4](无需调整)
2. 插入10 → [4,10] → 10与4交换 → [10,4]
3. 插入3 → [10,4,3](无需调整)
4. 插入5 → [10,4,3,5] → 5与4交换 → [10,5,3,4]
5. 插入1 → [10,5,3,4,1](无需调整)
最终堆结构:
10
/ \
5 3
/ \
4 1
代码实现:
int main() {
hp Heap;
HeapInit(&Heap);
int arr[] = { 4,10,3,5,1 };
for (int i = 0; i < sizeof(arr)/sizeof(int); i++)
{
HeapPush(&Heap, arr[i]);
}
HeapDestory(&Heap);
return 0;
}
时间复杂度分析:
- 每次插入需 O(logn) 时间(n 为当前堆大小)
- 遍历数组需O(n)
- 总时间 = O(nlogn)
2. 自底向上调整法(Bottom-up)
步骤:
- 直接填充数组:将所有元素按原始顺序放入数组。
- 从最后一个非叶子节点开始(叶子节点就是天然的一层堆,不需要调整):索引(size - 1 - 1) / 2
- 向前遍历:对每个节点执行 AdjustDown 操作。
初始数组:[3,5,2,10,4]
索引映射:
3(0)
/ \
5(1) 2(2)
/ \
10(3)4(4)
操作步骤:
1. 从索引1(元素5)开始调整 → 无需交换
2. 处理索引0(元素3):
- 比较子节点5和2 → 与5交换
- 交换后结构:
5(0)
/ \
3(1) 2(2)
/ \
10(3)4(4)
- 继续检查交换后的索引1(元素3) → 与10交换
最终堆结构:
10
/ \
5 2
/ \
3 4
代码实现:
void HeapCreate(hp* ph, HpDataType* arr, size_t size) {
assert(ph);
//给数组开辟空间
ph->arr = (HpDataType*)malloc(sizeof(HpDataType) * size);
if (ph->arr == NULL) {
perror("malloc err!");
return;
}
//拷贝传过来的数组
memcpy(ph->arr, arr, sizeof(HpDataType) * size);
ph->size = ph->capacity = size;
//调整堆的过程,从最后一个非叶子节点开始
for (int i = (size - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(ph->arr, size, i);
}
}
复杂度分析:
2. 堆排序
流程:
1)建堆 实现升序建大根堆 实现降序建小根堆
核心原理:
堆排序通过反复提取堆顶元素(极值)实现排序,堆的类型决定了提取元素的顺序:
- 大顶堆:堆顶始终为当前最大值
- 小顶堆:堆顶始终为当前最小值
2) 排序 利用堆删除思想排序
注:此图引自hello‑algo.com
代码实现:
void HeapSort(HpDataType* arr, int size) {
// 建堆 升序建大根堆 降序建小根堆
for (int i = (size - 2) / 2; i >= 0; i--) {
AdjustDown(arr, size, i);
}
// 排序
int end = size - 1;
while (end > 0) {
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
3. TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆(大的来了就替换堆顶最小值,重新修复堆结构,到最后就剩k个需要的数据) 前k个最小的元素,则建大堆(小的来了就替换堆顶最大值,重新修复堆结构,到最后就剩k个需要的数据)
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
- 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
代码实现:
void HeapSort(HpDataType* arr, int size) {
// 建堆 升序建大根堆 降序建小根堆
for (int i = (size - 2) / 2; i >= 0; i--) {
AdjustDown(arr, size, i);
}
// 排序
int end = size - 1;
while (end > 0) {
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
void DataCreate()
{
// 造数据
int n = 99;
srand(time(0));
FILE* fp = fopen("D:\\CODE\\DataStructure\\DataStructureClone\\Heap\\data.x", "w");
if (fp == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = (rand() + i) % 99;
fprintf(fp, "%d\\n", x);
}
fclose(fp);
}
void HeapTopK() {
//输入指令
printf("请输入k:");
int k = 0;
scanf_s("%d", &k);
//创建随机数据
DataCreate();
//读取文件中k个数据
FILE* fout = fopen("D:\\CODE\\DataStructure\\DataStructureClone\\Heap\\data.x", "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_s(fout, "%d", &minheap[i]);
}
//创建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minheap, k, i);
}
int x = 0;
while (fscanf_s(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);
}