文章目录
一、堆的概念及结构
有一个集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中 ,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,(即双亲比孩子的数值小(大))则称为小堆 ( 或大堆) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆的逻辑结构是一棵完全二叉树。
二、堆的实现
1. 结构的定义
堆的元素按完全二叉树的顺序存储方式存储在一维数组中,所以堆的结构和顺序表的结构一样。
typedef int HeapDataType;
typedef struct heap
{
HeapDataType* data;
int size;
int capacity;
}heap;
2. 堆的初始化
和顺序表一样,先在Init
函数外创建一个堆变量,再把堆的地址传进初始化函数,创建一个空堆。
void InitHeap(heap* ph)
{
assert(ph);
ph->data = NULL;
ph->capacity = 0;
ph->size = 0;
}
3. 堆的插入
堆只能在尾部进行插入,由于堆要求在插入元素后仍保持堆的性质,所以我们需要对堆进行向上调整,把新插入的元素调整到正确的位置,向上调整的过程其实也是建堆的过程。
//小根堆
void HeapPush(heap* ph, HeapDataType x)
{
assert(ph);
if (ph->capacity == ph->size) //扩容
{
int newcapacity = ph->data == NULL ? 4 : ph->capacity * 2;
HeapDataType* temp = (HeapDataType*)realloc(ph->data, sizeof(HeapDataType) * newcapacity);
if (temp == NULL)
{
perror("realloc");
exit(-1);
}
ph->data = temp;
ph->capacity = newcapacity;
}
ph->data[ph->size] = x;
//向上调整,保持堆结构
AdjustUp(ph->data, ph->size);
ph->size++;
}
4. 向上调整
这里以小根堆为例,如图:假设现在我们已经有了一个小根堆,现在我们往堆尾插入一个元素,那么可能会出现两种情况:
1、插入的元素大于父节点,此时我们的堆仍保持小根堆结构,所以不需要改动;比如我们往堆中插入30;
2、插入的元素小于父节点;这种情况下我们就需要把该节点不断往上调整,直到把堆调整为小根堆,最坏的情况是该节点被调整为根节点,比如我们插入10;
void Swap(HeapDataType* a, HeapDataType* b)
{
assert(a && b);
HeapDataType temp = *a;
*a = *b;
*b = temp;
}
//向上调整 - 小堆
void AdjustUp(HeapDataType* nums, int child)
{
assert(nums);
while (child > 0)//最坏调整到根节点停止
{
int parents = (child - 1) / 2;//父节点下标
if (nums[child] < nums[parents]) //交换条件
{
Swap(&nums[child], &nums[parents]);
child = parents; //子节点取代父节点下标
}
else //否则直接退出,此时已经满足堆结构
{
break;
}
}
}
如果我们需要调整大根堆,只需要把交换的条件修改一下即可。
if(nums[child] > nums[parents])
5. 堆的删除
对于堆的删除有明确的规定:我们只能删除堆顶的元素;但是头删之后存在两个问题:
1、顺序表头删需要挪动数据,效率低下;
2、挪动数据之后堆中各节点的父子关系完全破坏,很明显不满足堆的结构了;
对于上面的这些问题,我们有如下解决办法:
1、我们在删除之前先将堆顶和堆尾的元素交换,然后让size减一,这样相当于删除了堆顶的元素,且效率达到了O(1);
2、由于我们把堆尾元素交换到了堆顶,堆的结构遭到了局部破坏,所以需要设计一个向下调整算法来保持堆的结构;
//小根堆
void HeapPop(heap* ph)
{
assert(ph && ph->size > 0);
Swap(ph->data, ph->data + ph->size - 1);
ph->size--;
AdjustDown(ph->data, ph->size, 0);
}
6. 向下调整
堆向下调整的思路和向上调整刚好相反 (还是以小根堆为例):
1、找出子节点中较小的节点;2、比较父节点与较小子节点,如果父节点大于较小子节点则交换两个节点;3、交换之后,原来的子节点成为新的父节点,然后继续 1 2 步骤,直到调整为堆的结构。
注意:
向下调整算法比向上调整多一个参数
size
,因为向下调整需要判断子节点不能超过堆的范围向下调整算法有一个前提:左右子树必须都是堆,才能调整。
void Swap(HeapDataType* a, HeapDataType* b)
{
assert(a && b);
HeapDataType temp = *a;
*a = *b;
*b = temp;
}
//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
assert(nums);
int parents = sub;
int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
//当子结点超过堆的范围就结束
while (minchild < size)
{
//选出真正的较小子节点
if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
{
minchild++;
}
//如果父节点大于最小的子结点,就交换双方位置
if (nums[parents] > nums[minchild])
{
Swap(nums + parents, nums + minchild);
//迭代
parents = minchild;
minchild = parents * 2 + 1;
}
//否则不用向下调整,直接跳出循环
else
{
break;
}
}
}
和向上调整类似,如果我们想要向下调整为大堆,也只需要改变交换条件:
//选出真正的较大子节点
if (minchild + 1 < size && nums[minchild + 1] > nums[minchild])
//如果父节点小于最大的子结点,就交换双方位置
if (nums[parents] < nums[minchild])
7、取出堆顶的元素
HeapDataType HeapTop(heap* ph)
{
assert(ph && ph->size > 0);
return ph->data[0];
}
8、返回堆的元素个数
int HeapSize(heap* ph)
{
assert(ph);
return ph->size;
}
9、判断堆是否为空
bool IsHeapEmpty(heap* ph)
{
assert(ph);
return ph->size == 0;
}
10、打印堆中的数据
void PrintHeap(heap* ph)
{
assert(ph);
for (int i = 0; i < ph->size; i++)
{
printf("%d ", ph->data[i]);
}
printf("\n");
}
11、堆的销毁
void DestroyHeap(heap* ph)
{
assert(ph);
free(ph->data);
ph->capacity = 0;
ph->size = 0;
}
三、堆的应用
1. 堆排序
堆排序是选择排序的一种,它的时间复杂度为 O(N*logN
),空间复杂度为 O(1),是一种十分优秀的排序算法,主要步骤就是建堆和选择数。
建堆
建堆有两种方法:向上调整建堆和向下调整建堆。
向上调整建堆: 把数组的第一个元素作为堆的根节点,然后把其余元素看作插入结点在堆末尾插入,每插入一个元素就向上调整一次,从而始终保证堆的结构;
向上调整建堆的时间复杂度: 由于堆是完全二叉树,而满二叉树是完全二叉树的一种,所以此处为了简化计算,使用满二叉树来求时间复杂度
如上图,我们把每一层的节点个数乘以每一个节点需要调整的次数,最后再求和,就可以得到一共需要调整的次数;然后再根据满二叉树节点总数与树的高度的关系将表达式中的h替换掉,最终可以得到向上调整建堆的时间复杂度为:O(N*logN);
向下调整建堆: 从倒数第一个非叶子节点 (即最后一个叶子节点的父节点) 开始向下调整,直到调整到根。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dois3qci-1692531353764)(C:\Users\86185\AppData\Roaming\Typora\typora-user-images\image-20230818180009394.png)]
向下调整建堆的时间复杂度: 使用满二叉树来求时间复杂度
如上图,向下调整建堆的时间复杂度为:O(N),比向上调整的效率更高,这似乎有点反直觉,因为两者看起来并没有什么本质的不同,无非就是方向相反,但是简单分析就能发现原因,因为向下调整随着调整次数的增大,需要调整的结点越来越少,两者相乘就不会特别大,另外最后一层(接近一半的结点)是不需要调整的。
综合上面两种建堆方法,最好选择向下调整建堆,建堆的时间复杂度为:O(N);
选数
建堆完成后,接下来就是选数,假设现在我们要排降序,那么方法一共有两种:
1、建大堆:开辟一个和原数组同等大小的新数组,然后对原数组建大堆,每次取出堆顶的元素 (最大的元素) 按顺序放在新数组中,最后再将新数组中的数据覆盖至原数组;
缺点:需要开辟额外的空间,删除堆顶元素后向下调整的时间复杂度是O(logN),所以嵌套后时间复杂度:O(N*logN),空间复杂度:O(N);
2、建小堆:先对原数组建小堆,然后将堆顶和堆尾的数据进行交换,使得数组中最小的元素处于数组末尾,然后向下调整前n - 1个元素,使得次小的数据位于堆顶,最后重复前面的步骤,把次小的数据存放到最小的数据之前,直到数组有序;
优点:没有额外的空间消耗,且时间复杂度达到了 O(N*logN);
综合上面两种选数的方法,选数的时间复杂度为:O(N*logN),空间复杂度为:O(1);
void Swap(HeapDataType* a, HeapDataType* b)
{
assert(a && b);
HeapDataType temp = *a;
*a = *b;
*b = temp;
}
//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
assert(nums);
int parents = sub;
int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
//当子结点超过堆的范围就结束
while (minchild < size)
{
//选出真正的较小子节点
if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
{
minchild++;
}
//如果父节点大于最小的子结点,就交换双方位置
if (nums[parents] > nums[minchild])
{
Swap(nums + parents, nums + minchild);
//迭代
parents = minchild;
minchild = parents * 2 + 1;
}
//否则不用向下调整,直接跳出循环
else
{
break;
}
}
}
//升序 -> 建大根堆
//降序 -> 建小根堆
void HeapSort(int* nums, int numsSize)
{
//建堆method1:插入法 向上调整 O(N * logN)
/*for (int i = 1; i < numsSize; i++)
{
AdjustUp(nums, i);
}*/
//建堆method2:向下调整 O(N)
for (int i = (numsSize - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(nums, numsSize, i);
}
//选数
//降序 -> 小根堆 -> 向下调整 O(N * logN)
while (numsSize > 1)
{
Swap(nums, nums + numsSize - 1);
AdjustDown(nums, --numsSize, 0);
}
}
int main()
{
int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
HeapSort(nums, sizeof(nums) / sizeof(int));
for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
{
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
2. Topk
问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,无法排序;比如:世界500强、富豪榜、王者荣耀巅峰前十等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是,如果数据量非常大,排序就不太可取了 (数据都不能一下子全部加载到内存中),最佳的方式就是用堆来解决,基本思路如下:
用数据集合的前K个元素来建堆, 求前k个最大的元素,则建小堆,反之则建大堆;
用剩余的N-K个元素依次与堆顶元素来比较,满足条件则替换堆顶元素,并向下调整保持堆的结构;
void Swap(HeapDataType* a, HeapDataType* b)
{
assert(a && b);
HeapDataType temp = *a;
*a = *b;
*b = temp;
}
//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
assert(nums);
int parents = sub;
int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
//当子结点超过堆的范围就结束
while (minchild < size)
{
//选出真正的较小子节点
if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
{
minchild++;
}
//如果父节点大于最小的子结点,就交换双方位置
if (nums[parents] > nums[minchild])
{
Swap(nums + parents, nums + minchild);
//迭代
parents = minchild;
minchild = parents * 2 + 1;
}
//否则不用向下调整,直接跳出循环
else
{
break;
}
}
}
int* Topk(int* nums, int numsSize, int k)
{
int* ans = (int*)malloc(sizeof(int) * k);
if (ans == NULL)
{
perror("Topk");
exit(-1);
}
for (int i = 0; i < k; i++)
{
ans[i] = nums[i];
}
//向下建堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(ans, k, i);
}
for (int i = k; i < numsSize; i++)
{
if (nums[i] > ans[0]) //如果数组中其他元素大于堆顶元素
{
ans[0] = nums[i]; //替换堆顶元素
AdjustDown(ans, k, 0); //向下调整保持堆的结构
}
}
return ans;
}
int main()
{
srand((unsigned int)time(NULL)); //用时间戳产生随机数种子
int* nums = (int*)malloc(sizeof(int) * 10000);
if (nums == NULL)
{
perror("TestTopk");
exit(-1);
}
//生成10000个小于10000的整数
for (int i = 0; i < 10000; i++)
{
nums[i] = rand() % 10000;
}
//生成10个大于10000的整数
for (int i = 0; i < 10; i++)
{
nums[rand() % 10000] = 10000 + i;
}
int* ans = Topk(nums, 10000, 10);
for (int i = 0; i < 10; i++)
{
printf("%d ", ans[i]);
}
printf("\n");
free(ans);
ans = NULL;
free(nums);
nums = NULL;
return 0;
}
四、完整代码
Heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int HeapDataType;
typedef struct heap
{
HeapDataType* data;
int size;
int capacity;
}heap;
//初始堆
void InitHeap(heap* ph);
//打印堆
void PrintHeap(heap* ph);
//往堆里插入数据
void HeapPush(heap* ph, HeapDataType x);
//删除堆的首元素
void HeapPop(heap* ph);
//获取堆的首元素
HeapDataType HeapTop(heap* ph);
//判断堆是否为空
bool IsHeapEmpty(heap* ph);
//求堆的大小
int HeapSize(heap* ph);
//销毁堆
void DestroyHeap(heap* ph);
//堆排序
void HeapSort(int* nums, int numsSize);
//Topk
int* Topk(int* nums, int numsSize, int k);
Heap.c
#include"heap.h"
void InitHeap(heap* ph)
{
assert(ph);
ph->data = NULL;
ph->capacity = 0;
ph->size = 0;
}
void PrintHeap(heap* ph)
{
assert(ph);
for (int i = 0; i < ph->size; i++)
{
printf("%d ", ph->data[i]);
}
printf("\n");
}
void Swap(HeapDataType* a, HeapDataType* b)
{
assert(a && b);
HeapDataType temp = *a;
*a = *b;
*b = temp;
}
//向上调整 - 小堆
void AdjustUp(HeapDataType* nums, int child)
{
assert(nums);
while (child > 0)
{
int parents = (child - 1) / 2;
if (nums[child] < nums[parents])
{
Swap(&nums[child], &nums[parents]);
child = parents;
}
else
{
break;
}
}
}
//向下调整 - 小堆
void AdjustDown(HeapDataType* nums, int size, int sub)
{
assert(nums);
int parents = sub;
int minchild = parents * 2 + 1;//先默认最小孩子为左孩子
//当子结点超过堆的范围就结束
while (minchild < size)
{
if (minchild + 1 < size && nums[minchild + 1] < nums[minchild])
{
minchild++;
}
//如果父节点大于最小的子结点,就交换双方位置
if (nums[parents] > nums[minchild])
{
Swap(nums + parents, nums + minchild);
//迭代
parents = minchild;
minchild = parents * 2 + 1;
}
//否则不用向下调整,直接跳出循环
else
{
break;
}
}
}
//小根堆
void HeapPush(heap* ph, HeapDataType x)
{
assert(ph);
if (ph->capacity == ph->size) //扩容
{
int newcapacity = ph->data == NULL ? 4 : ph->capacity * 2;
HeapDataType* temp = (HeapDataType*)realloc(ph->data, sizeof(HeapDataType) * newcapacity);
if (temp == NULL)
{
perror("realloc");
exit(-1);
}
ph->data = temp;
ph->capacity = newcapacity;
}
ph->data[ph->size] = x;
AdjustUp(ph->data, ph->size);
ph->size++;
}
//小根堆
void HeapPop(heap* ph)
{
assert(ph && ph->size > 0);
Swap(ph->data, ph->data + ph->size - 1);
ph->size--;
AdjustDown(ph->data, ph->size, 0);
}
HeapDataType HeapTop(heap* ph)
{
assert(ph && ph->size > 0);
return ph->data[0];
}
bool IsHeapEmpty(heap* ph)
{
assert(ph);
return ph->size == 0;
}
int HeapSize(heap* ph)
{
assert(ph);
return ph->size;
}
void DestroyHeap(heap* ph)
{
assert(ph);
free(ph->data);
ph->capacity = 0;
ph->size = 0;
}
//升序 -> 大根堆
//降序 -> 小根堆
void HeapSort(int* nums, int numsSize)
{
//建堆method1:插入法 向上调整 O(N * logN)
/*for (int i = 1; i < numsSize; i++)
{
AdjustUp(nums, i);
}*/
//建堆method2:向下调整 O(N)
for (int i = (numsSize - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(nums, numsSize, i);
}
//降序 - 小根堆 - 向下调整 O(N * logN)
while (numsSize > 1)
{
Swap(nums, nums + numsSize - 1);
AdjustDown(nums, --numsSize, 0);
}
}
int* Topk(int* nums, int numsSize, int k)
{
int* ans = (int*)malloc(sizeof(int) * k);
if (ans == NULL)
{
perror("Topk");
exit(-1);
}
for (int i = 0; i < k; i++)
{
ans[i] = nums[i];
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(ans, k, i);
}
for (int i = k; i < numsSize; i++)
{
if (nums[i] > ans[0])
{
ans[0] = nums[i];
AdjustDown(ans, k, 0);
}
}
return ans;
}
test.c
#include"heap.h"
#include<time.h>
void TestHeap()
{
heap H;//创建堆变量
InitHeap(&H);
int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
{
HeapPush(&H, nums[i]);
}
PrintHeap(&H);
int n = sizeof(nums) / sizeof(int);
while (n--)
{
printf("%d ", HeapTop(&H));
HeapPop(&H);
}
}
void TestHeapSort()
{
int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
HeapSort(nums, sizeof(nums) / sizeof(int));
for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
{
printf("%d ", nums[i]);
}
printf("\n");
}
void TestTopk()
{
srand((unsigned int)time(NULL));
int* nums = (int*)malloc(sizeof(int) * 10000);
if (nums == NULL)
{
perror("TestTopk");
exit(-1);
}
for (int i = 0; i < 10000; i++)
{
nums[i] = rand() % 10000;
}
for (int i = 0; i < 10; i++)
{
nums[rand() % 10000] = 10000 + i;
}
int* ans = Topk(nums, 10000, 10);
for (int i = 0; i < 10; i++)
{
printf("%d ", ans[i]);
}
printf("\n");
free(ans);
ans = NULL;
free(nums);
nums = NULL;
}
int main()
{
//TestHeap();
//TestHeapSort();
TestTopk();
return 0;
}
}
void TestHeapSort()
{
int nums[] = { 10, 26, 85, 4, 23, 14, 12, 36, 16 };
HeapSort(nums, sizeof(nums) / sizeof(int));
for (int i = 0; i < sizeof(nums) / sizeof(int); i++)
{
printf("%d ", nums[i]);
}
printf("\n");
}
void TestTopk()
{
srand((unsigned int)time(NULL));
int* nums = (int*)malloc(sizeof(int) * 10000);
if (nums == NULL)
{
perror("TestTopk");
exit(-1);
}
for (int i = 0; i < 10000; i++)
{
nums[i] = rand() % 10000;
}
for (int i = 0; i < 10; i++)
{
nums[rand() % 10000] = 10000 + i;
}
int* ans = Topk(nums, 10000, 10);
for (int i = 0; i < 10; i++)
{
printf("%d ", ans[i]);
}
printf("\n");
free(ans);
ans = NULL;
free(nums);
nums = NULL;
}
int main()
{
//TestHeap();
//TestHeapSort();
TestTopk();
return 0;
}