堆(Heap)
本文主要介绍以下内容:
- Heap的实现
- HeapSort(堆排序)
- 完善各种堆的函数接口
- TopK经典问题
堆就是一棵完全二叉树。因为它的某些性质,我们可以用数组存储。
- 堆的性质
1 完全二叉树的性质。
(1)将根节点(树的最上面的结点)序号设为0,然后由左至右,由上至下编号。
(2)父节点编号若为i,左孩子编号为(21+1),右孩纸为(21+2);
(3)孩子的编号为i,父亲的编号则为 (i-1)/2 ;
2 父节点大于等于(或者小于等于)所有的子孙。
父节点大于等于子孙的堆叫大堆,反之叫做小堆。
不写代码的程序员不是好程序员,接下来,我们来实现Heap。
Heap的实现
//我们需要大量的函数接口
typedef int HeapDataType; //增加代码的移植性
typedef struct Heap{ //堆的结构
HeapDataType* _a; //数组
int _size; //堆的实际数据个数
int _capacity; //数组容量
}Heap;
//初始化堆
//hp是Heap结构体变量, a是数组 n是数组大小
void HeapCreat(Heap* hp ,HeapDataType* a ,int n);
//入堆
void HeapPush(Heap* hp ,HeapDataType x);
//从堆中删除
void HeapPop(Heap* hp);
//为了减少大家的恐惧,我们先写这三个(后面多着呢)。
首先大家应该思考一个问题,我随意给你一个数组,你怎样实现完全二叉树到堆的建立?即,怎样建堆?
这里,我们给大家来一个算法,向下调整算法,我们以建立一个小堆为例。假设有这样一个完全二叉树:
显然这不是一个小堆,因为0号(即编号为0的结点,为了方便叙述)大于1号,所以我将他俩换下位置。
我们先不管其他的,先盯着值为20的结点。我们发现,这时候1号比3和4都大,那么换谁呢?显然换3号,因为3号比4号小!
所以有,
同样的,3号大于7号和8号,交换7号(较小的)。
好了,经过这几步交换,看看我们得到了什么。
好像什么都没得到?不,我们将一个比较大的数字移到了下边,我们用的方法也值得我们将它写下来。
//向下调整算法
//三个参数:数组 ,数组大小 , 根节点
void AdjustDown(HeapDataType* a, int n, int root)
{
int parent = root;
int child = parent*2 + 1; //二叉树的性质
while(child < n)
if(child+1 < n && a[child] > a[child+1])//找出小的孩子
{
++child;
}
if(a[parent] > a[child]) //如果父亲大于孩子,则交换
{
int temp = a[parent];
a[parent] = a[child];
a[child] = temp;
parent = child; //迭代
child = parent*2 + 1;
}
else { //否则任务完成,直接跳出循环。
break;
}
}
我们在写的时候巧妙运用了 ++child,避免分左右孩子,接下来我们将用这个算法建堆。
我们意识到,如果从根节点向下调整不能建成小堆,但是,可以移动一个较大的数下沉,那么如果我从下开始用这个算法呢?
我们从最后一个有孩子的父节点(3号)开始会怎么样呢?
然后我们到第二个父节点,2号。
接着是1号父节点。
最后0号结点。
我们发现,成了!!!!
这就是我们想要的结果!
代码如下:
void HeapCreat(Heap* hp ,HeapDataType* a ,int n)
{
hp->_size = hp->_capacity = n;
hp->_a = (HeapDataType*)malloc(sizeof(HeapDataType));
if(hp->_a == NULL) return ; //防止开辟失败
memcpy(ph->_a,a,sizeof(HeapDataType)*n);//拷贝数据
//n-1是最后一片叶子的下标,然后利用性质求父节点,依次遍历前面的父节点。
for(int i = (n-1-1)/2 ; i >= 0; --i){
AdjustDown(hp->_a,hp->_size,i);
}
}
好了,小堆也已经建好,那么大堆呢? 显然易得,只需改变两个符号即可。
// a[child] < a[child+1];
// a[child] > a[parent] ;
好了,接下来,我们来看看堆能干什么呢?找最值!而且是连续的找!
那么这不就是排序吗?
所以接下来为大家介绍堆排序。
HeapSort ( 堆排序 )
我们以降序来举栗子。
首先有一个大问题摆在我们面前,降序使用大堆还是小堆?
这里给大家思考时间。
1
2
3
4
5
我就知道你会选择用大堆!降序不就是每次将最大的值选出来,排在数组头不久ok了?but,真是这样吗?
如果拿走100,然后呢?我们肯定想要那第二大的数作为根,于是70被拿了上来,
But,这样关系全乱了!!!
所以我们还得用向下调整法,但是时间复杂度相当高!
所以,我们用小堆。
我们先排成小堆,
然后怎么做呢?我们只知道4是最小的,它又要求降序,所以我们拿走4,
与最后一个结点交换。
就是
20 7 25 8 14 30 40 10 4
这时候最后一个数最小,然后我们当然要找第二小的。所以我们对除最后一个数进行重建小堆,然后再将根拿到倒数第二个位置,以此类推。堆排序就完成了。
//代码实现
void HeapSort(HeapDataType* hp->_a,int hp->_size)
{
for(int i = (hp->_size-2)/2;i>=0;i--)
{
AdjustDown(hp->_a,hp->_size,i); //建小堆
}
int end = hp->_size - 1;
while(end > 0){
Swap(&hp->_a[0],&hp->_a[end]); //交换函数 (我懒得写了(嘻嘻))
AdjustDown(hp->_a,end,0); //重建小堆
--end;
}
}
result。此时时候已经不早了,是时候来完善我们接下来的接口。
//void HeapSort(HeapDataType* a, int len);堆排
//void AdjustDown(HeapDataType* a, int n, int root); 向下调整
void AdjustUp(HeapDataType* a, int child); //向上调整
//void HeapCreat(Heap* hp, HeapDataType* a, int n); 建堆
void HeapPush(Heap* hp, HeapDataType x); //入堆
void HeapPop(Heap* hp); //从堆中移除
HeapDataType HeapTop(Heap* hp); //返回堆的根值
int HeapEmpty(Heap* hp); //判断堆是否为空
注释掉的是我们已经KO掉的,接下来我们挨个解决剩下的。
首先先来入堆。
怎么入堆呢?从根结点上入吗?那么原来的根结点放在哪里呢?
这里我们再来一种算法,向上调整法。
我们将x插入到堆的尾部,然后逐层向上调整。
结果:
//向上调整法
void AdjustUp(hp->_a,child)
{
int parent = (child - 1)/2;
while(child > 0) {
if(a[child] < a[parent]){
Swap(&hp->_a[child],&hp->_a[parent]);
child = parent;
parent = (child - 1)/2;
}
else break;
}
}
//我们想要将x插入到堆中,使得插入后还是一个堆。
//同样的,假设之前是一个小堆。
void HeapPush(Heap* hp, HeapDataType x)
{
//首先一个小问题,万一hp->_a 没空间怎么办?
if(hp->_size == hp->_capacity){ //空间满了。需要calloc
int newcapacity = hp->_capacity*2;
HeapDataType* HP = (HeapDataType*)calloc(hp->_a,sizeof(HeapDataType)*newcapacity);
if(HP == NULL){ //开辟失败
return ;
}
hp->_a = HP;
hp->_capacity = newcapacity; //开辟完成
}
hp->_a[hp->_size - 1] = x; //放入数据
hp->_size++; //size加一
AdjustUp(hp->_a,hp->_size-1);
}
出堆就比较简单,我们利用堆排的思想,将根节点与尾结点交换,然后向下调整即可。
void HeapPop(Heap* hp)
{
Swap(&hp->_a[0],&hp->_a[hp->_size-1]); //交换根和尾元素
hp->_size--; //干掉尾元素
AdjustDown(hp->_a,hp->_size,0); //向下调整。
}
剩下2个更是简单的一批。
HeapDataType HeapTop(Heap* hp)
{
return hp->_a[0];
}
int HeapEmpty(Heap* hp)
{
return hp->_size == 0 ? 1 : 0;
}
最后我们用堆来解决一道经典问题,TopK问题。
何为TopK问题? 从一堆数(大于k)中选出前k个大数(或小数)。
比如英雄联盟有一天说要给南京市第一亚索,一直到南京市第10亚索,给这10个玩家颁发奖励,他们先用一套算法算出每个压缩玩家的得分,然后怎么选出前k个呢?
自然而然的想法
- 排序
我不管你要求什么,只要是数组,排序可以解决大部分问题。实际上这题确实可以排序,但时间复杂度不尽人意,还有更优的解吗?能用堆吗? - 堆的性质
我先建一个大堆,找出最高分,然后再找次小的,知道k个玩家找完。这已经不错了,但是还有没有更优的解?或者说,我能不能建造小点的堆(堆的建立算法时间复杂度不是很easy)? - 更优的解法
任意取k个玩家的分数建造一个k的小堆,用其他玩家与根节点比较,如果比根节点大,则换掉根节点。然后向下调整成小堆,知道所有玩家比较完毕。
这里要注意的是,我只是找出前k个,并不知道这k个玩家谁更厉害(我没有给这k个玩家排序)。
而且,这并不是绝对的公平,想一想,如果所有人的分数都相同,这个算法不就是随机抽10个人吗?
这就是堆的大部分问题,由于是晚上写的博客,加上对截图工具使用实在粗糙,图有点模糊,如有问题,希望不要介意。
(全文完)