使用堆排序求前k个最大(小)数
原理
假设序列中有n个元素,取其前k个组成一个最大堆。由于最大堆的堆顶为序列中最大元素,所以组成的最大堆的堆顶是前k个元素中最大的元素。依次用第k+1到第n个元素与堆顶进行比较,如果比堆顶元素大,那么该元素肯定不会是前K个最小的元素;如果比堆顶小,那么堆顶的元素肯定不是前k个最小的元素,此时更新堆顶元素,并重新计算使新堆成为最大堆。依次进行,直到第n个元素结束,那么留在堆内的元素就是前k个最小的元素。
对于求前k个最小的元素,思想与上面差不多。只不过要取前k个元素建成最小堆,剩余的元素依次与堆顶元素相比,大则更新堆顶,并重新排列堆;小则放弃。直到n个元素比较完成,留在堆中的就是前k个最大元素。
代码
/*
count : 整个数组的长度
k : 要获取前k个最小数
*/
void HeapSort(int a[],int count,int k){
if(k > count)
return;
int b[k];
int x = 0;
for(x =0;x<k;x++){
b[x] = a[x];
}
siftup(b, k);//先将这些数字变成最大堆
// printarray(b, k);
for(x = k;x<count;x++){
if(a[x] < b[0]){
b[0] = a[x];
AdjustAtPos(b,k,0);
}
}
printarray(b, k);
}
上述代码是求前k个最小值的代码,它的思路为:首先取a数组中前k个数组成b数组,并通过siftup()方法将b数组转换成最大堆。再从第k+1个元素开始,依次和堆顶元素比对,如果堆顶元素大,则更新堆顶元素,并重新计算移动堆顶元素,使新堆仍旧是最大堆。
siftup()如下:
void siftup(int a[],int count){
int x = 0;
if(count % 2 == 0){
x = count/2-1;
}else{
x = (count-1)/2;
}
for(;x>=0;x--){
AdjustAtPos(a, count, x);
}
}
前面的判断是用来寻找最后一个非叶子节点(如果二叉树根节点记为编号记为0,那么编号为k的节点其左右子节点的编号分别为2k+1,2k+2)。并依次调整每一个非叶子节点的位置。
AdjustAtPos()如下:
void AdjustAtPos(int a[],int count,int pos){
int index = pos;
while(2*pos+1 < count){
if(a[pos] < a[2*pos+1])
index = 2*pos+1;
if(2*pos + 2<count){
if(a[pos] < a[2*pos + 2]){
index = 2*pos + 2;
}
}
if(index != pos){
swap(a, index, pos);
pos = index;
}else{
break;
}
}
}
while判断如果成立表明该节点肯定有左节点。
第一个if判断如果成立表明当前节点的值小于其左节点的值,用index记录下该节点的编号。如果不成立,那么index就是pos。这个判断执行完毕之后,index存储的就是根节点和左节点中值比较大的节点的编号。
第二次if判断成立的话,表明其有右节点。内层的判断逻辑与上面一样。
上述代码执行完毕后,index指的就是根节点,左、右节点中值比较大的节点的编号(下标)。因此,如果index == pos成立,那么意味着根节点是最大的,不需要进行调整。如果不成立,并假设index记录的是左节点的下标,那么就需要交换左节点与根节点的值(swap()方法的作用),交换完毕之后左子树就不一定是最大堆了,又需要重新调用左子树,这就是pos = index的作用。
拓展
在上面的求前k个最小数时,建立的是最大堆,直到序列的最后一个元素遍历结束后,堆顶仍旧是堆中最大的元素。因此堆顶是第k个最小的元素。利用这,就可以求出第k大或第k小的元素。
求前k个最小元素或者第k小的元素,使用最大堆,堆的节点个数为k,堆顶的元素就是第k个最小的元素。求前k个最大元素或第k个最大元素,利用最小堆,堆的节点个数为k,堆顶的元素就是第k个最大的元素。