目录
堆的概念及性质
堆的本质是一个完全二叉树
大堆:树中所有父亲结点都大于等于孩子结点
小堆:树中所有父亲结点都小于等于孩子结点
如上图,实际上堆的存储结构,是存在数组中的,即是它的物理结构(在内存中的存储)
孩子和父亲下标的关系:
leftchild=parent*2+1 左孩子一定是奇数
rightchild=parent*2+2 右孩子一定是偶数
而若已知子结点要推父节点:
parent=(child-1)/2
注意:堆不一定有序,上面的有序只是巧合,因为堆的性质中并没有限制左右孩子的大小关系
例题:
思路:任何一个数组都可以看作一个完全二叉树,但不一定是堆,因此可以把数组按照1、2、4…逐个的分解出来,再来检查是否满足堆的性质
堆的函数声明与实现
函数声明
堆的逻辑结构是一个完全二叉树,而它的物理存储结构却是一个数组,我们采用变长数组,用size代表堆中元素个数,用capacity代表数组空间的大小,三者构成堆的结构
typedef int HPDataType;
typedef struct Heap
{
//堆的存储结构由数组实现
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php); //初始化
void HeapDestroy(HP* php); //销毁
void AdjustUp(HPDataType* a, int child); //向上调整
void AdjustDown(HPDataType* a, int n, int parent); //向下调整
void HeapPush(HP* php, HPDataType x); //插入
void HeapPop(HP* php); //删除根(堆顶)
HPDataType HeapTop(HP* php); //取堆顶
int HeapSize(HP* php); //堆大小
bool HeapEmpty(HP* php); //堆是否为空
函数实现
1.初始化
数组这种存储结构的初始化很简单,我们只用考虑在初始化时是否预先为数组开辟一小段空间,这里我们不开空间
void HeapInit(HP* php){
assert(php);
php->a=NULL;
php->capacity=php->size=0;
}
2.销毁
free掉数组占用的空间,并将数组指针置为空防止野指针造成的非法内存访问
void HeapDestroy(HP* php){
assert(php);
free(php->a);
php->a=NULL;
php->size=php->capacity=0;
}
3.插入
堆的插入,在物理存储上是存在数组的下一个位置里(尾插),但要满足堆的性质,因此在插入时,插入的值的影响范围是它的所有祖先结点;若不满足堆的性质,我们需要对它向上调整
向上调整
这里以小堆为例:
假设插入的数据是6,堆是小堆,因此要满足父节点小于子节点,因此要让6与8换位置,换位置后,再比较6与9,若还是父节点大于子节点,则再换一次位置,如此循环,直到满足堆的性质,循环结束
此时我们插入的位置是数组的尾部,最后一个元素下标为size-1,用parent=(child-1)/2,这一关系即可定位它的父节点下标,比较、调整(交换)即可;由于这里涉及到多次交换操作,因此我们不妨将它封装到函数里
void Swap(HPDataType* a,HPDataType* b){
int tmp=*a;
*a=*b;
*b=tmp;
}
void AdjustUp(HPDataType* a, int child){
int parent=(child-1)/2;
while(child>0){
//这里我们将它调整为大堆
if(parent<child){
Swap(&child,&parent);
child=parent;
parent=(child-1)/2;
}
//说明此时已经满足堆的性质,调整结束
else{
break;
}
}
}
此时我们再来考虑插入,①首先插入数据我们需要考虑数组空间是否足够,不够则需要对它进行扩容;②然后再把待插入数据放入堆中;③最后让插入后满足堆的性质进行向上调整
void HeapPush(HP* php, HPDataType x){
assert(php);
//检查空间
if(php->size==php->capacity){
int newCapacity=php->capacity==0?4:capacity*2;
HPDataType* tmp=(HPDataType*)realloc(php->a,sizeof(DataType)*newCapacity);
if(tmp==NULL){
printf("realloc fail");
exit(-1);
}
php->a=tmp;
php->capacity=newCapacity;
//放入数据
php->a[size]=x;
php->size++;
//向上调整
AdjustUp(php->a,php->size-1);
}
}
4.删除
由于堆的结构性质,堆顶代表整个堆中最大或最小的数据,因此我们删除只考虑对堆顶的删除;若直接删除堆顶,即相当于数组的头删,删除后数组元素向前移动,那么会打破原有堆结点间的关系,所以我这里不是直接删除;我们用堆顶的元素与堆尾的元素交换位置,此时尾删即可删除堆顶数据;交换后由于不再满足堆原有的性质,所以这里我们需要用到向下调整
向下调整
向下调整算法基本思想(以建成大堆为例):
①从根节点开始,选出左右孩子节点中值较大的一个
②让父亲与较大的孩子比较,若父亲小于此孩子,那么交换;若父亲大于此孩子,则不交换;
③当父亲大于等于较大的孩子则停止,或调整到叶子节点也要停止(叶子节点特征为没有左孩子,就是数组下标超出了范围,就不存在了)
需要用到的关系:**leftchild=parent*2+1,rightchild=parent*2+2 **
对于向上调整,当调整到根节点就不能在继续循环下去了,而向下调整最后的停止条件是最后一个叶子结点,因此在函数传参时我们还需要传递结结点的个数,方便控制循环
void AdjustDown(HPDataType* a, int n, int parent) {
//左右孩子中较大的于父节点作比较
int child=parent*2+1;
while(child<n){
//这里我们将它调整为大堆
//child代表左右孩子中较大的那一个
if(child+1<n&&a[child+1]>a[child]){
child++;
}
//比较、交换
if(a[parent]<a[child]){
Swap(&a[parent],&a[child]);
parent=child;
child=parent*2+1;
}
//若已满足堆性质,提前结束循环
else{
break;
}
}
}
此时我们再来考虑删除,删除前我们首先要检查数组是否为空,再考虑删除的步骤:①首先交换堆顶和堆尾数据,②然后尾删,③最后再向下调整即可
void HeapPop(HP* php){
assert(php);
assert(php->size>0);
//①交换
Swap(&php->a[0],&php->a[php->size-1]);
//②删除
php->size--;
//③调整
AdjustDown(php->a,php->size,0);
}
5.取堆顶
HPDataType HeapTop(HP* php) {
assert(php);
assert(php->size > 0);
return php->a[0];
}
对于大堆来说,堆顶的元素最大,因此我们可以采用【取堆顶-删除堆顶】(删除堆顶后会调整为新的大堆)的循环方式,来取出这个数组的前k个最大的数据:
int main(){
int array[] = { 27,15,19,18,28,34,65,49,25,37 };
HP hp;
HeapInit(&hp);
int size = sizeof(array) / sizeof(HPDataType);
//插入
for (int i = 0; i < size; i++) {
HeapPush(&hp, array[i]);
}
//取最大的前5个元素
for (int i = 0; i < 5; i++)
{
//取堆顶-删除堆顶
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
6.堆的大小
int HeapSize(HP* php) {
assert(php);
return php->size;
}
7.堆是否为空
bool HeapEmpty(HP* php) {
assert(php);
return php->size == 0;
}
堆函数的应用
通过对前面堆的函数的复用,我们可以实现①堆的创建、②堆排序以及③解决TOP—K问题
void HeapCreat1(HP* php, HPDataType* a, int size); //堆的创建(复用函数)
void HeapCreat2(HP* php, HPDataType* a, int size); //堆的创建(更高的效率)
void HeapSort(int* a, int size); //堆排序/建堆(向上调整)
void HeapTOPK(int k); //TOP-K问题
1.堆的创建
方案1
我们可以循环复用插入函数,插入k次即可创建一个含有k个数据的堆
void HeapCreat1(HP* php,HPDataType* a,int size){
assert(php);
HeapInit(php);
for(int i=0;i<size;i++){
HeapPush(php,a[i]);
}
}
方案2
建堆算法1:向下调整
对于向下调整算法,需要满足条件:这个结点的左孩子和右孩子都要满足堆的性质,然后从这个结点开始使用向下调整算法即可建堆;因此这里我们不妨将最后一个结点看成一个堆,那么这个结点的父结点就可以执行向下调整算法,因此在建堆时,我们从最后一个结点的父节点开始,依次向上执行向上调整算法,那么即可将数组建立为堆
void HeapCreat2(HP* php,HPDataType* arr,int size){
assert(php);
HeapInit(php);
//由于初始化堆时并没有对数组开空间,因此需要我们为堆开辟空间
php->a=(HPDataType*)malloc(sizeof(HPDataTYpe)*size);
if(php->a==NULL){
perror("malloc fail");
exit(-1);
}
//将待建堆数组复制到堆结构的数组中
memcpy(php->a,arr,sizeof(HPDataType)*size);
php->size=php->capacity=size;
for(int i=(size-1-1)/2;i>=0;i--){
AdjustDown(php->a,size,i);
}
}
建堆算法2:向上调整
向上调整算法的使用时,需要满足原先是一个堆,在堆尾插入通过调整使其成为一个新的堆;因此这里我们不妨假设数组首元素是一个堆,从第二个开始依次插入,并执行向上调整
for(int i=1;i<size;i++){
AdjustUp(a,i);
}
2.堆排序
我们对一个数组进行堆排序,首先要把数组建堆,然后再排序;虽然排序前也有【建堆】的操作,然而与建堆不同的是,只建堆需要我们开辟空间,将提供的数组中的内容看作数据,把这些内容memcpy到我们开辟的空间中,然而堆排序的目标即是提供的数组,因此不用再开辟空间,直接对提供的数组进行操作即可**(不用单独开辟空间)**
建堆后为什么就能排序了呢,因为对于大堆来说,堆顶的元素是最大的,我们可以运用这一性质:每次让堆顶和堆底数据交换,然后对除了堆底位置执行向下调整,那么第二大的数据就到了堆顶,在与倒数第二个位置数据交换,再对除最后两个位置执行向下调整,依次循环直到堆顶
void HeapSort(int* a,int size){
//建堆
for(int i=1;i<size;i++){
AdjustUp(a,i);
}
//排序
int end=size-1;
while(end>0){
Swap(&a[0],&a[end]);
AdjustDown(a,end,0);
end--;
}
}
这样的堆排序时间复杂度极低,仅为:O(N*logN)
3.TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决
方案1
建立一个N个数的大堆,Pop K次,依次取堆顶
缺点:假设N很大,那么建立N个数的大堆,需要开辟一片非常大的空间(100亿的整数需要40G的空间,内存空间不够,而在磁盘中不能建堆)——存在空间问题
方案2
①用前k个数建立K个数的小堆,②依次遍历数据,比堆顶数据大,就替换堆顶,再向下调整;③最后在数组中的最大的前k个数即存在堆中(数据存在文件——硬盘中,解决了空间问题)
分析:由于是小堆,每一个进入的数,如果是相对较大的数都会向堆底移动,而堆顶是堆中元素相对较小的那一个,因此到最后遍历完数组,堆顶保存的即是整个数组中的第K大的数(大数在堆中下沉)
每一次替换,都用较大的存入堆中,最后遍历完后即得到的就是最大的前K个值
因为TOP-K问题的数据量很大,因此我们将待排序的数据存到文件中,因此这里涉及到对文件的操作
void HeapTOPK(int k) {
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL) {
perror("malloc fail");
exit(-1);
}
//①读文件
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
//②取文件的前k个数据
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
}
//③将这k个数据建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(minHeap, k, i);
}
//④读取文件所有数据,与堆顶作比较
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, k, 0);
}
}
//⑤打印排序好的前K个大数
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
printf("\n");
fclose(fout);
}