目录
一、堆的概念及结构
堆(Heap)是一种特殊的数据结构,通常以完全二叉树的形式呈现。
堆的核心特性是:
- 对于大根堆,任意父节点的值都大于或等于其子节点的值
- 对于小根堆,任意父节点的值都小于或等于其子节点的值
堆通常使用数组来模拟完全二叉树的结构。数组索引按照从上到下、从左到右的方式对应树的节点排列。假设某个节点在数组中的位置为i
,则其左子节点和右子节点的位置分别为 2*i+1
和 2*i+2
,而其父节点的位置为(i-1)/2
。
如下所示:
二、小根堆的实现
2.1 堆的数据结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
2.2 堆的初始化HeapInit
void HeapInit(Heap* php) {
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
2.3 堆的销毁HeapDestory
// 堆的销毁
void HeapDestory(Heap* php) {
assert(php);
free(php->a);
php->capacity = php->size = 0;
}
2.4 堆的插入HeapPush
假如,我们现在有一个数组a:15 18 19 25 28 34 65 49 27 37,他从逻辑上看,是一个小根堆结构,我们将它的逻辑结构画出来如下所示:
我们现在想要将10插入堆里面去,插入10之后,要不允许破坏原本小根堆的结构,所以我们就需要将10一步一步的调整,如下所示:
2.4.1 插入代码HeapPush
// 堆的插入
void HeapPush(Heap* php, HPDataType x) {
assert(php);
if (php->capacity == php->size) {
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL) {
perror("HeapPush()::realloc");
exit(1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
2.4.2 向上调整代码AdjustUp
//向上调整,建立大根堆
void AdjustUp(HPDataType* a, int child) {
assert(a);
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
}
child = parent;
parent = (child - 1) / 2;
}
2.4.3 交换数据代码Swap
//交换函数
void Swap(HPDataType* x, HPDataType* y) {
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
2.5 堆的删除HeapPop
堆的删除,我们首先要确定删除哪一个元素,看上面的数组a:10 15 19 25 18 34 65 49 27 37 28.我们画出他的二叉树表现形式,如下所示,他满足小根堆特性。
现在数组中有这么多的元素,我们要删除哪一个呢?最常见的是删除第一个元素和最后一个元素,如果我们删除最后一个元素28,删除的没有意义,他既不是最大的也不是最小的,所以堆的删除一般都是删除堆顶元素,也就是以一个元素,删除堆顶元素之后,后面的元素需要向前移动吗?答案是不能,如果向前移动,元素之间的逻辑关系会改变,不满足小堆特性,所有如果我们要删除第一个元素,通常做法是将最后一个元素与第一个元素互换,只有第一个元素不满足小根堆的特性,其他元素都满足,我们还需要写一个向下调整算法。
2.5.1 删除代码HeapPop
// 堆的删除
void HeapPop(Heap* php) {
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, 0, php->size);
}
2.5.2 向下调整算法AdjustDown
void AdjustDown(HPDataType* a, int parent, int size) {
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] < a[child]) {
child++;
}
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
}
parent = child;
child = 2 * parent + 1;
}
}
2.6 取堆顶的数据HeapTop
// 取堆顶的数据
HPDataType HeapTop(Heap* php) {
assert(php);
assert(php->size > 0);
return php->a[0];
}
2.7 获取堆的数据个数HeapSize
// 堆的数据个数
int HeapSize(Heap* php) {
assert(php);
return php->size;
}
2.8 堆的判空HeapEmpty
// 堆的判空
int HeapEmpty(Heap* php) {
return php->size == 0 ? 1 : 0;
}
三、堆的应用
3.1 堆排序
如果我们想要升序的话,就需要建立大根堆。
如果我们想要降序的话,就需要建立小根堆。
现在我们有一个数组a{ 27,15,19,18,28,34,65,49,25,37 };我们想要将a进行从小到大排序,所以我们就需要对这个数组进行建堆处理。
那现在我们如何建堆呢?
方法一:我们可以访问数组的每一个元素,然后初始化一个堆,将数组的每一个元素插入到堆里面,我们根据堆的自动调整算法,可以自动的变成大根堆,然后我们取出堆顶的元素,把它放在数组里面,然后堆的删除,重复n次。但是这样空间复杂度太高了需要重新新建一个堆。
方法二:我们可以从数组的第二个元素开始,进行向上调整算法,就这个数组变成一个堆,如下所示:
for (int i = 1; i < size; i++) {
AdjustUp(a, i);
}
我们打开我们的调试,看到数组a成功的变成了一个大根堆。但是这样时间复杂度太高了。
方法三:可以从最后一个分支节点开始,依次进行向下调整算法,如下所示:
for (int i =(size-2)/2 ; i >= 0; i--)//
{
AdjustDown(a, i, size);
}
我们常用的建堆算法是方法三。
建完堆之后,我们将堆顶元素与最后一个元素互换,然后重新调整堆,重复n次。代码如下所示:
int end = size - 1;
while (end > 0) {
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
这样我们就成功的将数组a从小到大排序了。
3.2 TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
3.2.1 创建大量数据
我们使用代码创建大量的数据,如下所示:
void CreatData() {
FILE* pf = fopen("data.txt", "w");
if (pf == NULL) {
perror("fopen");
exit(1);
}
int num = 100000;
for (int i = 0; i < num; i++) {
int number = rand()+i;
fprintf(pf, "%d\n", number);
}
}
3.2.2 求K个最大的元素
首先,输入k的值,malloc出k个int类型的空间,然后从文件中读取前k个数,将他们放在malloc出来的空间里面,将他们建成小堆,最后依次读取文件中后n-k个,依次与堆顶进行比较,如果大于堆顶就替换,然后调整堆。代码如下:
void TOP_K() {
printf("请输入k:");
int k = 0;
scanf("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL) {
perror("malloc fail");
exit(1);
}
//从文件中拷贝k个数到堆里面
FILE* pf = fopen("data.txt", "r");
if (pf == NULL) {
perror("fopen");
exit(1);
}
for (int i = 0; i < k; i++) {
fscanf(pf, "%d", &kminheap[i]);
}
//建立k个数的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(kminheap, i, k);
}
//读取剩下的n-k个数据
int x = 0;
while (fscanf(pf,"%d", &x)>0) {
if (x > kminheap[0]) {
kminheap[0] = x;
AdjustDown(kminheap, 0, k);
}
}
printf("最大的前%d个数", k);
for (int i = 0; i < k; i++) {
printf("%d ", kminheap[i]);
}
printf("\n");
}
3.2.3 验证
我们现在已经把最大的前k个输出了,我们如何验证这k个元素就是十万个数据中最大的前10个呢?
我们在创建数据的时候可以%10000000,来控制我们的数据,创建完成数据之后,我们在data.txt文档中随机的修改k个数,在它们后面加上4个0,这样我们就知道k个最大的数后面肯定有4个0。我们调用top-k函数,如下所示:
四、代码
4.1 堆的代码
4.1.1 Heap.h
#pragma once
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
void AdjustDown(HPDataType* a, int parent, int size);
void AdjustUp(HPDataType* a, int child);
//交换函数
void Swap(HPDataType* x, HPDataType* y);
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
4.1.2 Heap.c
#include"Heap.h"
void HeapInit(Heap* php) {
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
// 堆的销毁
void HeapDestory(Heap* php) {
assert(php);
free(php->a);
php->capacity = php->size = 0;
}
//交换函数
void Swap(HPDataType* x, HPDataType* y) {
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
//向上调整,建立大根堆
void AdjustUp(HPDataType* a, int child) {
assert(a);
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] > a[parent]) {
Swap(&a[child], &a[parent]);
}
child = parent;
parent = (child - 1) / 2;
}
}
// 堆的插入
void HeapPush(Heap* php, HPDataType x) {
assert(php);
if (php->capacity == php->size) {
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL) {
perror("HeapPush()::realloc");
exit(1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int parent, int size) {
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] > a[child]) {
child++;
}
if (a[child] > a[parent]) {
Swap(&a[child], &a[parent]);
}
parent = child;
child = 2 * parent + 1;
}
}
// 堆的删除
void HeapPop(Heap* php) {
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, 0, php->size);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* php) {
assert(php);
assert(php->size > 0);
return php->a[0];
}
// 堆的数据个数
int HeapSize(Heap* php) {
assert(php);
return php->size;
}
// 堆的判空
int HeapEmpty(Heap* php) {
return php->size == 0 ? 1 : 0;
}
4.2 堆排序的代码
//交换函数
void Swap(HPDataType* x, HPDataType* y) {
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
//向上调整,建立大根堆
void AdjustUp(HPDataType* a, int child) {
assert(a);
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] > a[parent]) {
Swap(&a[child], &a[parent]);
}
child = parent;
parent = (child - 1) / 2;
}
}
void AdjustDown(HPDataType* a, int parent, int size) {
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] > a[child]) {
child++;
}
if (a[child] > a[parent]) {
Swap(&a[child], &a[parent]);
}
parent = child;
child = 2 * parent + 1;
}
}
void HeapSort(int* a, int size) {
for (int i =(size-2)/2 ; i >= 0; i--)//
{
AdjustDown(a, i, size);
}
int end = size - 1;
while (end > 0) {
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
}
int main() {
int a[] = { 27,15,19,18,28,34,65,49,25,37 };
int size = sizeof(a) / sizeof(a[1]);
HeapSort(a, size);
for (int i = 0; i < size; i++) {
printf("%d ", a[i]);
}
return 0;
}
4.3 TOP-K的代码
//交换函数
void Swap(HPDataType* x, HPDataType* y) {
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustDown(HPDataType* a, int parent, int size) {
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] < a[child]) {
child++;
}
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
}
parent = child;
child = 2 * parent + 1;
}
}
void CreatData() {
FILE* pf = fopen("data.txt", "w");
if (pf == NULL) {
perror("fopen");
exit(1);
}
int num = 100000;
for (int i = 0; i < num; i++) {
int number = (rand()+i)%10000000;
fprintf(pf, "%d\n", number);
}
}
void TOP_K() {
printf("请输入k:");
int k = 0;
scanf("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL) {
perror("malloc fail");
exit(1);
}
//从文件中拷贝k个数到堆里面
FILE* pf = fopen("data.txt", "r");
if (pf == NULL) {
perror("fopen");
exit(1);
}
for (int i = 0; i < k; i++) {
fscanf(pf, "%d", &kminheap[i]);
}
//建立k个数的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(kminheap, i, k);
}
//读取剩下的n-k个数据
int x = 0;
while (fscanf(pf,"%d", &x)>0) {
if (x > kminheap[0]) {
kminheap[0] = x;
AdjustDown(kminheap, 0, k);
}
}
printf("最大的前%d个数", k);
for (int i = 0; i < k; i++) {
printf("%d ", kminheap[i]);
}
printf("\n");
}
int main() {
srand((unsigned int)time(NULL));
CreatData();
TOP_K();
return 0;
}