1.1堆的概念及结构
堆的原型是二叉树的顺序结构,只适用于存储完全二叉树,所以堆总是一颗完全二叉树,堆分为两种,将元素按完全二叉树的顺序存储方式存储在一个一维数组中,也就是从以而二叉树的根节点为数组下标0开始,从左至右从上至下,二叉树所存储数据的数组下标依次加1。将满足根节点所存储的数据大于等于左右子结点所存储的数据则称为大根堆(或大堆),满足根节点所存储的数据小于等于左右子结点所存储的数据则称为小根堆(或小堆)。对于堆,根结点所存储的数据一定都要满足大于等于(小于等于)左右子结点的要求,否则不能称之为堆。
2.1堆的实现
2.11用顺序表模拟实现一个堆
通过熟悉的顺序表建立一个堆,让我们更加熟悉堆的相关知识。
堆结构的定义:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;//数据存储
int size;//堆中数据个数
int capacity;//堆容量
}HP;
首先我们要清楚,堆的结构是顺序结构,可以使用顺序表来实现一个堆,我们将堆看成一个动态开辟内存的数组,同时要知道该数组的容量大小以及所储存数据的个数,所以可以定义两个变量来记录数组的相关数据。
堆的初始化:void HPInit(HP*php);
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
对堆的简单初始化,将指针域置空,数组容量与储存数据个数赋值为0,。
数据入堆:void HPPush(HP*php,HPDataType x);
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)//堆容量不够,扩容
{
int newCapacity = (php->capacity == 0 ? 4 : php->capacity * 2);
HPDataType* temp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
if (temp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = temp;
php->capacity = newCapacity;
}
//数据入堆
php->a[php->size] = x;
php->size++;
//向上调整堆
AdjustUp(php->a, php->size-1);
}
数据入堆,首先需要对堆的容量进行检查,如果容量大小不够则需要及时扩容,这里是每次扩容为之前容量的两倍, 在确定容量大小足够的情况下,我们再将数据入堆,在这之前我们所做的操作与顺序表的操作完全相同,而接下来的则是建成一个堆的关键操作,我们每次将数据入堆时,都需要对,堆的数据位置进行调整,确保为大堆(小堆)。这个操作我们在数据入堆是,将数据从下往上调整位置的,所以称为向上调整堆。
向上调整堆:
void AdjustUp(HPDataType* a, int child)
{
assert(a);
HPDataType parent = (child - 1) / 2;//得到要调整结点下标
while (child > 0)
{
if (a[parent] < a[child])//调整为大堆
{
//交换根节点与子结点的数据
HPDataType temp = a[parent];
a[parent] = a[child];
a[child] = temp;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。向上调整堆,需要传递数组,以及入堆数据的数组下标大小,将堆数据自下而上调整,这里我们有一个一定需要知道的点是,子结点的数组下标大小与根节点的下标关系,知道根节点的数组下标就可以知道其左右子结点的数组下标,同样知道其左右子结点的下标也能知道父结点的下标,这点在前面对树以及二叉树的介绍也提到过,在知道了这点之后我们就可以很容易调整堆数据位置,将不满足条件的数据位置交换改变,如果是要调整为大堆,则需要将大于根结点的子结点数据与根结点交换,小堆同理。
堆的销毁:void HPDestroy(HP* php);
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->size = php->capacity = 0;
}
由顺序表建立的堆,只需要释放动态开辟的内存即可。
2.12由数组定义一个堆
实际上,我们不需要顺序表就能建立一个堆,可以将一个容量大小固定的数组看成一个堆,围绕该数组本身来建立一个堆,我们给出一个数组,这个数组在逻辑上可以看做一颗完全二叉树,但并不是一个真正的堆,我们可以通过算法,将它构建成一个想要的大(小)堆。
堆向下调整的算法:
顺序表中的向上调整的算法并不适用当前场景,我们可以通过从根节点开始的向下调整算法可以把它调整成一个小堆,但向下调整算法有一个前提:左右子树必须是一个堆,才能调整。我们需要先将根节点的左右子树先调整为堆,这里我们从倒数的第一个非叶子节点的子树开始调整,将每一颗子树都调整为堆,一直调整到根节点的树,就可以将整棵树调整成堆,完成堆的建立。
void Swap(int* p1, int* p2)//交换两数
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void HeapSet(int* a, int n)//逐一向下调整为大堆
{
int pos = (n - 1 - 1) / 2;//得到调整起始下标
while (pos>=0)//调整至根结点
{
int parent = pos;
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[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
pos--;//指向下一结点调整
}
}
3.1堆的运用
3.11堆排序
我们知道,堆分为两种:一种是大堆,一种是小堆,堆的根结点的数据与子结点之间一定满足大于等于,或者小于等于的关系,而前面我们使用向下调整建堆的算法中,我们依次将倒数第一个非叶子结点的子树到最后的整颗树调整为堆,不断的将更大或者跟小的数据往上交换位置,而在这一过程,实际上,我们不能发现,在大堆中,树的根结点储存的数据是整棵树最大的数据,而在小根堆中,根结点储存的数据是最小的,而这一信息则能恰好的被运用到排序中。
假定有一个储存n个数据的数组,将其建成一个大堆,我们可以通过将大堆中根结点的数据(数组下标为0的元素)与堆中最末尾的数据进行交换(数组最后一个元素),从而将最大的数据置于数组最后位置,然后再将堆大小减一,使其不再能够调整访问最末尾的数据,最后再讲得到的前n-1个数据向下调整为新的堆,这样就又得到了第二大的数据,反复如此,就得到了一组升序的数据;而要得到一个降序的数据,则同样可以建立一个小堆,进行上述操作。
void Swap(int* p1, int* p2)//交换两数据
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void HeapSort(int* a, int n)//逐一向下调整为大堆
{
int pos = (n - 1 - 1) / 2;//得到调整起始下标
while (pos>=0)
{
int parent = pos;
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[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
pos--;//指向下一结点调整
}
Swap(&a[0],&a[n - 1]);
n--;
if (n > 0)
{
HeapSort(a, n);
}
}
3.12小顶堆解决TopK问题
关于TopK问题,是指在大量数据中找出出现频率最高的前k个数,或者从海量数据中找出最大的前k个数,这类问题通常被称为TopK问题。由于数据的庞大性,排序在此类问题上不能很好的解决,而今天介绍的堆则恰好能够很好的解决TopK问题。
思路:
- 首先,我们可以建立一个可以储存k个数据的小堆。
- 其次,我们将需要筛选的数据的前k个数据导入我们已经建好的小堆中。
- 然后,我们再依次遍历剩下的数据,与小堆中根结点储存的元素比较,如果大于根结点储存的元素,则将该数据取代根节点的数据进入堆,重新调整堆,反复如此,直到遍历完整个数据。此步骤为最为关键的一步,通过遍历剩余数据,将更大的数据取代进堆,会将小的数据从堆中移除,而堆顶的根结点中的数据为整个堆中最小的数据,这样,将堆中最小的数据移除,重新调整堆,不断将堆中最小的数据移除出去,而大的数据沉入堆底,直到堆中栈顶为第k大的数据时,剩余数据将不再能够进入堆中,将堆的入口堵住,这样就筛选出了前k大的数据。
- 最后,我们所建立的小堆中的k个元素就是所有数据中前k大的数据,如果还需要对剩下的k个元素排序,我们则可以使用前面介绍过的堆排序,最后依次打印数据则可形成升序(降序)的前k大数据。
void Swap(int* p1, int* p2)//交换两数
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void AdjustDown(int* a, int sz, int n)//向下调整为小堆
{
int parent = n;//调整起始下标
int child = n * 2 + 1;//假设左子结点中数据更小
while (child < sz)
{
if (child+1 < sz && a[child] > a[child+1])//找到存储数据更大的子结点
{
child++;
}
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void SetHeap(int* a, int k, int sz)
{
int* hp = (int*)malloc(sizeof(int) * k);//开辟储存k个单位的小堆
if (hp == NULL)//开辟失败
{
perror("malloc fail");
exit(-1);
}
//将a中前k个数据存入堆中
int i = 0;
for (i = 0; i < k; i++)
{
hp[i] = a[i];
}
//调整数据为小堆
for (i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(hp, k, i);
}
//遍历数据,大于根数据的取代进堆,将更大的数据沉入堆底
i = k;
while (i < sz)
{
if (hp[0] < a[i])
{
hp[0] = a[i];//取代进堆
AdjustDown(hp, k, 0);
}
i++;
}
//找到前第前n大个数据
for (i = 0; i < k; i++)
{
printf("%d ", hp[i]);
}
printf("\n");
i = k;
//堆排序
while (i > 0)
{
Swap(&hp[0], &hp[i - 1]);
i--;
AdjustDown(hp,i,0);
}
for (i = 0; i < k; i++)
{
printf("%d ", hp[i]);
}
}
例如: 利用srand函数生成10w随机数进行筛选前5个最大数:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define M 100000
void Init(int* a, int sz)
{
for (int i = 0; i < sz; i++)
{
a[i] = rand() % 1000 + 1;
}
a[123] = 555555;
a[234] = 666666;
a[345] = 777777;
a[456] = 888888;
a[567] = 999999;
}
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void AdjustDown(int* a, int sz,int n)//向下调整为小堆
{
int parent = n;//调整起始下标
int child = n * 2 + 1;//假设左孩子更小
while (child < sz)
{
if (child+1 < sz && a[child] > a[child+1])//调整孩子大小
{
child++;
}
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void SetHeap(int* a, int k, int sz)
{
int* hp = (int*)malloc(sizeof(int) * k);//开辟储存k个单位的堆
if (hp == NULL)//开辟失败
{
perror("malloc fail");
exit(-1);
}
//将a中前k个数据存入堆中
int i = 0;
for (i = 0; i < k; i++)
{
hp[i] = a[i];
}
//调整数据为小堆
for (i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(hp, k, i);
}
//遍历数据,大于根数据的取代进堆,将更大的数据沉入堆底
i = k;
while (i < sz)
{
if (hp[0] < a[i])
{
hp[0] = a[i];//取代进堆
AdjustDown(hp, k, 0);
}
i++;
}
//找到前第前n大个数据
for (i = 0; i < k; i++)
{
printf("%d ", hp[i]);
}
printf("\n");
i = k;
//堆排序
while (i > 0)
{
Swap(&hp[0], &hp[i - 1]);
i--;
AdjustDown(hp,i,0);
}
for (i = 0; i < k; i++)
{
printf("%d ", hp[i]);
}
}
int main()
{
srand((unsigned)time(NULL));
int a[M];
Init(a, M);
SetHeap(a, 5, M);
return 0;
}
运行结果:
结语:
本期关于堆的相关知识到这就介绍完了,如果感觉对你有帮助的话还请点个赞支持一下!有不对或者需要改正的地方还请指正,感谢各位的观看。