堆的实现与应用:从基础操作到排序与 Top-K 问题
堆作为一种特殊的完全二叉树,间距数组的储存效率和二叉树的逻辑特性,是数据结构中解决排序、Top-K等问题的力利器。本文将结合理论原理、完整代码实现和实际应用场景,带大家全面掌握堆的核心知识。
一、堆的核心概念和特性
堆是满足特定规则的完全二叉树,底层通常采用数组顺序存储,主要分为两种类型:
小根堆:每个父节点的值小于等于其左右子节点的值,根节点为整个堆的最小值。
大根堆:每个父节点的值大于等于其左右子节点的值,根节点为整个堆的最大值。
完全二叉树的关键性质:
1. 对于下标为 i 的节点,其父节点的下标为(i-1)/2
2. 对于下标为 i 的节点,其左孩子下标为 2*i+1 ,右孩子下标为 2*i+2
二、堆的核心操作实现
这里用小根堆来进行演示。
1.数据结构定义
首先定义堆的结构体,包括存储数据的数组、当前元素个数和容量
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* a; // 存储堆元素的数组int size; // 当前堆中元素个数int capacity; // 堆的最大容量
} Hp;2.向上调整算法
用于堆的插入操作,维持小根堆性质
1. 新元素插入到数组尾部(堆的最后一个位置)
2. 不断与父节点比较,若小于父节点则交换,指导满足堆性质或到达根节点
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; // 满足堆性质,退出调整}}
}3. 向下调整算法
用于堆的删除操作,维持小根堆的性质
1. 调整的前提是左右子树已满足堆性质,删除堆顶元素
2. 将堆顶元素与最后一个节点交换位置,不会破坏根节点左右子树的结构
3. 选择左右子节点中较小的一个,与父节点比较,若子节点更小则交换,递归调整子树。
void AdJustDown(HPDataType* a, int n, int parent)
{int child = 2 * parent + 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 = 2 * parent + 1;} else {break; // 满足堆性质,退出调整}}
}4. 堆的完整操作
(1)初始化与销毁
// 初始化堆
void HeapInit(Hp* hp)
{assert(hp);hp->a = NULL;hp->size = hp->capacity = 0;
}// 销毁堆,释放内存
void HeapDestory(Hp* hp)
{assert(hp);free(hp->a);hp->a = NULL;hp->capacity = hp->size = 0;
}(2)插入与删除
// 插入元素到堆
void HeapPush(Hp* hp, HPDataType x)
{assert(hp);// 容量不足时扩容(初始容量为4,之后翻倍)if (hp->size == hp->capacity) {int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;hp->a = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));hp->capacity = newcapacity;}hp->a[hp->size] = x; // 尾部插入AdJustUp(hp->a, hp->size); // 向上调整hp->size++;
}// 删除堆顶元素(堆的核心元素)
void HeapPop(Hp* hp)
{assert(hp);assert(hp->size > 0);Swap(&(hp->a[0]), &(hp->a[hp->size - 1])); // 堆顶与最后一个元素交换hp->size--; // 删除最后一个元素(原堆顶)AdJustDown(hp->a, hp->size, 0); // 向下调整堆顶
}(3)查询与判断
// 获取堆顶元素
HPDataType HeapTop(Hp* hp)
{assert(hp);assert(hp->size > 0);return hp->a[0];
}// 获取堆的大小
int HeapSize(Hp* hp)
{assert(hp);return hp->size;
}// 判断堆是否为空
bool HeapEmpty(Hp* hp)
{assert(hp);return hp->size == 0;
}三、堆的两大核心应用
1.堆排序
利用堆的性质实现排序,时间复杂度 O (n log n)
升序排序:构建大根堆,每次将堆顶(最大值)与末尾元素交换,再调整前 n-1 个元素为大根堆。
降序排序:构建小根堆,逻辑与升序类似。
void HeapSort(int* a, int size)
{// 1. 构建堆(从最后一个非叶子节点的父节点开始向下调整)for (int i = (size - 1 - 1) / 2; i >= 0; i--) {AdJustDown(a, size, i);}// 2. 排序:逐步将堆顶元素移到末尾int end = size - 1;while (end > 0) {Swap(&(a[0]), &(a[end])); // 堆顶(最大值)与末尾交换AdJustDown(a, end, 0); // 调整前end个元素为堆end--;}
}2.Top-K问题(求前K个最大值)
对于海量数据(无法全部加载到内存),堆是解决 Top-K 问题的最优方案。
1. 用数据集合中前 K 个元素来建堆
(1)前 K 个最大的元素,则建小堆
(2)前 K 个最小的元素,则建大堆
2. 用剩余的 N-K 个元素一次于堆顶元素来比较,满足条件就覆盖根,然后调整
3. 将剩余 N-K 个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素
// 生成海量测试数据(100万个随机数)
void creatdata()
{int n = 1000000;FILE* file = fopen("data.txt", "w");if (file == NULL) {perror("fopen");return;}srand((unsigned int)time(NULL));for (int i = 0; i < n; i++) {int x = (rand() + i) % 100000;fprintf(file, "%d\n", x);}fclose(file);
}// 求解Top-K问题
void topk()
{creatdata(); // 生成数据FILE* fout = fopen("data.txt", "r");if (fout == NULL) {perror("fopen");return;}int k = 0;printf("请输入要查找的最大元素个数:");scanf("%d", &k);// 开辟K个元素的数组,存储前K个数据int* arr = (int*)malloc(k * sizeof(int));for (int i = 0; i < k; i++) {fscanf(fout, "%d", &arr[i]);}// 构建小根堆for (int i = (k - 1 - 1) / 2; i >= 0; i--) {AdJustDown(arr, k, i);}// 遍历剩余数据,更新堆int x = 0;while (fscanf(fout, "%d", &x) != EOF) {if (x > arr[0]) { // 大于堆顶(当前最小值)则替换arr[0] = x;AdJustDown(arr, k, 0); // 调整堆}}// 输出前K个最大值printf("前%d个最大值为:", k);for (int i = 0; i < k; i++) {printf("%d ", arr[i]);}printf("\n");free(arr);fclose(fout);
}要判断上面的代码是否正确,可以在生成数据文件后在 K 个数据后面加上一些数字(不要溢出),他们就是最大的。如果结果中是这些数字,证明是对的。
