一、定义
堆(Heap)是
计算机科学
中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看做一棵
完全二叉树
的数组对象。
二、功能实现
<0>头文件
#ifndef HEAP_H
#define HEAP_H
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <string.h>
#endif
typedef int HPDatatype;
typedef struct Heap
{
HPDatatype *a;
int size;
int capacity;
} HP;
void HPCreat(HP *php,HPDatatype *a,int n);//堆的构建
void HPInit(HP *php);//初始化堆
void HPPrint(HP *php);//打印堆的数据
void HPDestroy(HP *php);//销毁堆
void Swap(HPDatatype *p1,HPDatatype *p2);//交换元素
void AdjustUp(HPDatatype *a,int child);//向上调整
void HPPush(HP *php,HPDatatype x);//往堆插入元素
void AdjustDown(HPDatatype *a,int size,int parent);//向下调整
void HPPop(HP *php);//删除堆顶数据
HPDatatype HPTop(HP *php);//返回堆顶数据
int HPSize(HP *php);//返回堆的大小
bool HPEmpty(HP *php);//判断堆是否为空
void HeapSort(int *a,int n);//堆排序算法
<1>堆的构建
void HPCreat(HP *php,HPDatatype *a,int n)//堆的构建(利用一个数组a)
{
assert(php);
php->a=(HPDatatype*)malloc(sizeof(HPDatatype)*n);
if(!php->a)
{
perror("malloc fail");
exit(-1);
}
//自底向上建堆算法
//只需要复制数组数据,然后自底向上(bottom-up)把树由小到大不断向下调整,最终就可以得到堆
//时间复杂度为O(N)
//如果把数组里的数据一个个往堆里插,想当于自顶向下(top-down建堆算法)不断向上调整,时间复杂度为O(N*log N)
memcpy(php->a,a,n*sizeof(HPDatatype));
php->size=php->capacity=n;
for(int i=(n-1-1)/2;i>=0;i--)
{
AdjustDown(php->a,n,i);
}
}
建堆有两种方法:
使用向上调整自顶向下(top-down)建堆,时间复杂度为O(N*logN)
使用向下调整自底向上(bottom-up)建堆,时间复杂度为O(N)
注:使用向上调整不可以自底向上建堆,使用向下调整不可以自顶向下建堆,无法实现大小堆的要求,可以自己找几个例子试一试
<2>初始化堆
void HPInit(HP *php)//初始化堆
{
assert(php);
php->a=NULL;
php->size=php->capacity=0;
}
<3>打印堆的数据
void HPPrint(HP *php)//打印堆的数据
{
assert(php);
for(int i=0;i<php->size;i++)
{
printf("%d ",php->a[i]);
}
printf("\n");
}
<4>销毁堆
void HPDestroy(HP *php)//销毁堆
{
assert(php);
free(php->a);
php->a=NULL;
php->size=php->capacity=0;
}
<5>交换元素
void Swap(HPDatatype *p1,HPDatatype *p2)//交换元素
{
HPDatatype tmp=*p1;
*p1=*p2;
*p2=tmp;
}
<6>向上调整
void AdjustUp(HPDatatype *a,int child)//向上调整,时间复杂度为log N
{
int parent=(child-1)/2;
while(child>0)
{
if(a[child]<a[parent])//此处>为大堆,<为小堆
{
Swap(&(a[child]),&(a[parent]));
child=parent;
parent=(parent-1)/2;
}else
break;
}
}
<7>往堆插入元素
void HPPush(HP *php,HPDatatype x)//往堆插入数据
{
//堆的插入操作时间复杂度为log N
assert(php);
if(php->size==php->capacity)
{
int newcapacity=(php->capacity == 0 ? 4 : php->capacity*2);
HPDatatype *tmp=(HPDatatype*)realloc(php->a,sizeof(HPDatatype)*newcapacity);
if(!tmp)
{
perror("realloc fail");
exit(-1);
}
php->a=tmp;
php->capacity=newcapacity;
}
php->a[php->size++]=x;
AdjustUp(php->a,php->size-1);//每次往堆插入数据时,都要判断向上调整一次,这样不管插入的数据的大小顺序怎么样,最终建立起来的堆都直接排好了顺序
}
<8>向下调整
void AdjustDown(HPDatatype *a,int size,int parent)//向下调整,时间复杂度为log N
{
int child=parent*2+1;
while(child<size)
{
//此处需要判断哪个是数据更小的孩子
if(child+1<size&&a[child]>a[child+1])
child++;
if(a[parent]>a[child])
{
Swap(&(a[child]),&(a[parent]));
parent=child;
child=child*2+1;
}else
break;
}
}
<9>删除堆顶数据
void HPPop(HP *php)//删除堆顶数据
{
assert(php);
assert(php->size>0);
Swap(&(php->a[0]),&(php->a[--php->size]));
AdjustDown(php->a,php->size,0);
}
<10>返回堆顶数据
HPDatatype HPTop(HP *php)//返回堆顶数据
{
assert(php);
assert(php->size>0);
return php->a[0];
}
<11>返回堆的大小
int HPSize(HP *php)//返回堆的大小
{
assert(php);
return php->size;
}
<12>判断堆是否为空
bool HPEmpty(HP *php)//判断堆是否为空
{
assert(php);
return php->size==0;
}
<13>堆排序算法
void HeapSort(int *a,int n)//堆排序算法
{
//向下调整建堆
for(int i=(n-1-1)/2;i>=0;i--)
{
AdjustDown(a,n,i);
}
//排序
int end=n-1;
while(end>0)
{
Swap(&a[0],&a[end]);
AdjustDown(a,end,0);
end--;
}
}
分析:若目标顺序为升序,感觉建小堆更好,但是发现建小堆也只能找到最小的数据,而其他数据仍然是乱序的。为了以升序的方式存储数据,没有办法把最小的元素插到堆尾,因此我们不得不再开一个堆的空间,空间复杂度为O(N),并且每次把堆顶元素pop掉再重新向下调整一次堆,时间复杂度为O(N*logN)。
而假如不开空间,即把小堆堆顶的数据从数组头部开始存储,我们需要不断地重新建堆,因为除去堆顶元素后,数组里剩余的数据是混乱的,整个树的结构都被破坏了,因此最后时间复杂度为O(N^2)。
因此实际上升序排序我们需要建的是大堆,才能最高效地解决问题。反之,降序排序我们需要建的是小堆。
升序排序:建大堆(O(N)),选出最大的数据,与堆的新堆尾处数据交换位置,堆不断缩小。并且每交换一次数据,从堆顶进行一次向下调整使新的堆仍然是大堆(因为两个最大的子树仍然为大堆)。最终,数组中存储的数据就变成了升序排序。每次向下调整的时间复杂度为O(logN),移动N个数据,则该算法时间复杂度为O(N*logN)
降序排序:建小堆(O(N))选出最小的数据,与堆的新堆尾处数据交换位置,堆不断缩小。并且每交换一次数据,从堆顶进行一次向下调整使新的堆仍然是小堆(因为两个最大的子树仍然为小堆)。最终,数组中存储的数据就变成了降序排序。每次向下调整的时间复杂度为O(logN),移动N个数据,则该算法时间复杂度为O(N*logN)
三、实例
main函数:
top k问题:
建一个N个数据的堆,取k次堆顶,同时pop k次堆顶,再向下调整,时间复杂度为O(logN)
当数据过大时,数据不能全部放到内存中。建一个k个数据的堆,然后遍历(N-k)个数据。
若取最大的k个数据,则堆为小堆,遍历中若数据大于堆顶数据则替换堆顶,并向下调整。
最终这个堆中的k个数据则为最大的k个数据。最小的k个数据同理。
#include "heap.h"
void Topk(int k)
{
int maxheap[k];
FILE *fout=fopen("heapdata.txt","r");
if(!fout)
{
perror("fopen fail");
exit(-1);
}
for(int i=0;i<k;i++)
{
fscanf(fout,"%d",&maxheap[i]);
}
for(int i=(k-1-1)/2;i>=0;i--)
{
AdjustDown(maxheap,k,i);
}
int val=0;
while(fscanf(fout,"%d",&val)!=EOF)
{
if(val>maxheap[0])
{
maxheap[0]=val;
AdjustDown(maxheap,k,0);
}
}
for(int i=0;i<k;i++)
{
printf("%d ",maxheap[i]);
}
printf("\n");
fclose(fout);
}
int main()
{
HP hp;
int arr[5]={5,4,3,2,1};
HPCreat(&hp,arr,5);
HPPrint(&hp);
printf("heap size:%d\n",HPSize(&hp));
printf("heap top:%d\n",HPTop(&hp));
HPPush(&hp,0);
printf("newtop:%d\n",HPTop(&hp));
HPPop(&hp);
HPPrint(&hp);
HPPop(&hp);
HPPop(&hp);
HPPop(&hp);
HPPop(&hp);
HPPop(&hp);
if(HPEmpty(&hp))
printf("empty\n");
Topk(5);
HeapSort(arr,5);
for(int i=0;i<5;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
HPDestroy(&hp);
return 0;
}
heap.txt数据:
12
313
32
231
2001
21
1000
55
667
33
22
33
2000
44
34
76
2
9
2005
22
2000
99
80
67
4424
311
运行结果: