C语言实现堆及其运用
文章目录
一、堆的概念及结构
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值
2.堆总是一棵完全二叉树
并且值得注意的是 堆的逻辑结构和物理结构截然不同
如图:
并且这个结构还有一个非常特殊的规律
父子间下标规律:
父亲找孩子:leftchild = parent * 2 + 1 ------ rightchild = parent * 2 + 2
孩子找父亲:parent = (child - 1) / 2
这个规律极其重要 它帮助了我们控制堆
这时聪明的小伙伴应该已经猜到了堆应该是用数组来实现的
至于为什么用数组 可以参考两者之间时间和空间的复杂度的区别
二、堆的实现
1.堆节点的定义
根据堆总是一棵完全二叉树的这一特征 我们给出堆节点的定义
typedef int HeapDataType;
typedef struct heap
{
HeapDataType* a;//存储的数据
int size;//堆的大小
int capacity;//堆的最大容量
}heap;
2.堆的创建
void HeapCreate(heap* php, HeapDataType* a, int n)
{
assert(php);
HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
php->a = tmp;
php->size = 0;
php->capacity = n;
}
3.堆的销毁
void HeapDestory(heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
4.堆的插入
注意:这里我们实现的堆是从尾部插入的所以我们用向上调整法 并且实现的是小堆序
void HeapPush(heap* php, HeapDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->capacity = newcapacity;
php->a = tmp;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size, php->size - 1);
}
5.向上调整和向下调整
无论的向上调整还是向下调整 我们都需要满足调整前的结构是一个合法的堆
void AdjustUp(HeapDataType* a, int n, 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(HeapDataType* 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 = child * 2 + 1;
}
else
{
break;
}
}
}
6.堆的删除
这里需要额外注意的是删除是指删除堆顶数据,并且堆顶数据不能够直接删除,如果直接删除就会破坏堆原有的结构,导致整个结构无法使用,所以我们将堆的最后一个数据与堆顶数据交换 变相将堆顶数据删除 然后让堆顶数据向下调整 保持原有的大堆/小堆结构
void HeapPop(heap* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
7.取堆顶数据
HeapDataType HeapTop(heap* php)
{
assert(php);
return php->a[0];
}
8.堆的数据个数
int HeapSize(heap* php)
{
assert(php);
return php->size;
}
9.堆的判空
int HeapEmpty(heap* php)
{
assert(php);
return php->size == 0 ? 1 : 0;
}
三、堆的运用
这里我们主要介绍堆排序以及TopK问题
1.堆排序
堆排序的原理其实很简单,因为堆就是用数组来实现的,所以需要把这个数组变成大堆或者小堆
但由于堆只规定父子之间的关系,并不在意兄弟之间数据的大小,单单只建堆是完成不了排序的
但堆顶的数据一定是整个堆中最大或者最小的那个,由此我们可以将堆顶的数据与堆的最后一个数据(假设为第N个)进行调换,然后将前N-1个数据进行向下调整 循环操作N-1次 完成排序
根据以上操作 我们不难得出以下结果
升序———建大堆
降序———建小堆
void HeapSort(int* a, int n)
{
// 建堆
int i = 0;
for (i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
// 排序
int end = n - 1;
while (end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
2.TopK问题
TopK问题是指在N个数中获得K个最大或者最小的数
如果我们用普通方法必定需要进行排序,当N很大时,这时的时间与空间的复杂度会变得很高,排序的成本很大
所以我们可以建一个大小为K的大堆或者小堆 当数据比堆顶数据大或者小的时候选择入堆,而当走完N个数据之后
堆中的数据就是我们需要的K个数据
void DataCreat(int n)
{
srand((unsigned)time(NULL));
FILE* fp = fopen("data.txt", "w");
int i = 0;
for (i = 0; i < n; i++)
{
fprintf(fp, "%d\n", rand() % 10000);//这里我们随机生成数据
}
fclose(fp);
}
void PrintTopK(int* a, int k)
{
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void TestTopk()
{
//创造随机数
int N = 1000;
DataCreat(N);
int K = 10;
HeapDataType* kminheap = (HeapDataType*)malloc(sizeof(HeapDataType) * K);
if (kminheap == NULL)
{
perror("malloc file");
return;
}
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen file");
return;
}
for (int i = 0; i < K; i++)
{
fscanf(fp, "%d", &kminheap[i]);
}
for (int i = (K - 2)/2; i >= 0; i--)
{
AdjustDown(kminheap, K, i);
}
for (int i = K; i < N; i++)
{
int val = 0;
fscanf(fp, "%d", &val);
if (val > kminheap[0])
{
kminheap[0] = val;
AdjustDown(kminheap, K, 0);
}
}
fclose(fp);
PrintTopK(kminheap, K);
}