目录
写在前面的话
友友们大家好啊!今天的主题是关于堆排序的两个应用:topk问题以及堆排序问题。topk问题在日常生活中也是很常见的,比如我们需要在一个几万甚至几十万的数中找出最大的前十个数,那么如果将他们全部加载到内存中去排序的话,明显是不行的,因为内存没有那么大。
所以这里我们就用到了堆结构的的特性来解决这个问题。接下来我们一起看看吧。
一,topk 问题
那么对于堆如何实现一组很大的数中选出最大或者最小的前 k 个数呢?
1.1思路一实现
首先我们的思路是这样的,将这组数中的前面 k 个数建堆,这里我们以选出前 k 个最大的数为例。
步骤一
那么对于建堆操作,我们一定需要用到的是向上或者向下调整 ,这里为了统一,我们统一用向下调整,然后将数组中下标为 0~9 的十个数依次建堆。
同时我们考虑到的一个问题是,如果我们想要选出前 k 个最大的数,那么一定需要建小堆,这样我们能保证堆顶的元素一定是最小的,而其他的元素一定比它大,如下图所示:
注意在建小堆的过程中,因为用的是向下调整建小堆,所以最后一个参数传递的应该是孩子节点的下标;如果这里我们用的是向上调整的话,则需要传递的是双亲节点的下标。(如有小伙伴对于向下调整和向上调整有疑虑,请移步上篇文章哦!)
步骤二
接下来我们将剩余的元素依次与堆顶元素比较,如果比堆顶元素大,那么则交换两个值,然后再调用一次向下调整。直到所有元素比较完,最后得到的便是一个最大的前 k 个数的一个小堆。
代码实现
//找出前 k 个最大数
void test_topk()
{
int k =8;
int a[] = {12,34,65,67,1,4,135,234,567,3444,5685};
int sz = sizeof(a) / sizeof(a[0]);
//1.将前 k 个数建小堆
for (int i = 0; i < k; i++)
{
ADJustDown(a, k, (i - 1 - 1) / 2);
}
//2.将剩余的数依次与堆顶的数比较,如果大就换,最后得到的便是最大的前 k 个数
for (int i = k; i < sz; i++)
{
if (a[i] > a[0])
{
Swap(&a[i],&a[0]);
ADJustDown(a,k,0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ",a[i]);
}
}
1.2思路二实现
那么因为我们是通过堆实现的,所以我们可以通过堆的一些接口实现。
其实大概思路是相同的,首先我们还是需要建小堆(或大堆),但是不是通过向下调整或者向上调整建的。而是通过堆尾插入操作实现的,当然,在堆尾插入的时候还是需要调堆。
其次,剪完堆之后的第二步,我们是在比较完了之后,如果堆顶元素小于当前位置元素,首先将堆顶元素删除,然后再将该元素插入,那么同样的,因为在插入元素的函数中已经实现了调堆操作,所以这里我们不需要过多的其他操作。
这样两步之后,就实现了 topk 问题。
tips:我们需要知道的是,对于数组而言,其实就相当于是一个堆了,只是是一个顺序按照数组元素顺序存放的堆,所以我们可以不用进行通过堆的接口函数去实现建堆的操作了,但是向上调整和向下调整依旧是进行了建堆操作,所以其实道理是一样的。
代码实现
//堆排序
//堆排序一般不用创建堆。数组就可以当做堆,只需要进行调整即可。
void HeapSort(int* a, int sz)
{
HP hp;
HeapInit(&hp);
//1.先建一个大堆
for (int i = 0; i<sz; i++)
{
ADJustDown(a, sz, (i-1)/2);
}
for (int i = sz - 1; i > 0; i--)
{
//2.将最大的那个元素,也即堆顶元素与最后一个节点交换。
Swap(&a[0], &a[i]);
ADJustDown(a, i, 0);
}
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
}
二,堆排序
那么上面我们通过对 topk 问题的实现,我们大概理解了对于建堆以及调堆的操作。接下来我们实现一下堆排序。
2.1堆排序思路
这里我们以升序做例子。
主要思路:
首先,如果我们想排升序,那么我们需要创建一个大堆,因为此时堆顶的元素一定是最大的(小堆的话,就是最小的),然后我们将堆尾元素与堆顶元素互换。最后我们再将不包括堆尾元素(该元素此时是最大值)的大堆再进行一次调堆。
然后重复上面的步骤,直到最后只剩一个元素。最后得到的便是一个升序的小堆,也就完成了我们的排序操作。
2.2思路实现
根据上述思路,这里我们依旧不打算采用堆的一些接口,因为那样做的代价是比较简单的,所以我们依旧只是用向下调整去实现。
//堆排序
//堆排序一般不用创建堆。数组就可以当做堆,只需要进行调整即可。
void HeapSort(int *a,int sz)
{
//1.先建一个大堆
for (int i = 0; i <sz ; i++)
{
ADJustDown(a,sz,(i-1)/2);
}
for (int i = sz-1; i > 0 ; i--)
{
//2.将最大的那个元素,也即堆顶元素与最后一个节点交换。
Swap(&a[0],&a[i]);
ADJustDown(a, i,0);
}
for (int i = 0; i < sz; i++)
{
printf("%d ",a[i]);
}
}
那么对于这里的第二个循环,因为每一次交换堆顶元素和堆尾元素之后,需要将不包括最后一个元素的堆再次进行一次调堆操作。所以这里每次传递给向下调整函数的数组元素个数是变化的,所以循环变量 i 是倒着变得。
2.3复杂度及稳定性
因为堆排序是通过堆来选数的,所以效率会高很多。这里们求出其时间复杂度为O(N^logN),同样是没有开辟空间的,所以空间复杂度是O(1)。
那么因为在建完堆之后,调堆操作会将最大数或者最小数移动到堆尾,所以每个元素的相对位置都可能发生变化,所以是不稳定的。
好的,那么本文到此就结束啦!如有问题,还请指正呀!