堆的应用(堆排序TopK问题)
文章目录
- 1. 堆排序
- 1)建堆的思考(时间复杂度计算)
- 2)利用堆删除思想来进行排序
- 3)实现堆排序(升序-大堆)
- 4)运行效果
- 5)分析建堆的时间复杂度
- 一)向上调整算法【最终近似O(NlogN)】
- 二)向下调整算法【最终近似O(N)】
- 三)选数字排序【O(NlogN)】
- 2. TOP-K问题(小堆)
- 1)基本思路
- 2)实现代码,要建立小堆
- 3)运行效果
1. 堆排序
堆排序可以帮助选数,本质是选择排序。
1)建堆的思考(时间复杂度计算)
利用向下调整算法和向上调整算法完成建堆,第一个数作为一个堆,后面的数依次进行插入,本质是模拟堆插入的过程
升序:建大堆;降序:建小堆
为什么是这样子的呢?拿升序建大堆为例进行讲解:
假设给出一个数组,要使用堆排序实现升序,如果这时建小堆,那么数组第一个数是最小的,那后面的数就需要重新建堆,而一次建堆的时间复杂度本身就是
O(NlogN)
,还需要重复n
次这样的操作,即使看上去高大上,但实际效率十分低。
而采用大堆进行升序时,能够确定最大的数,这样就可以对数组首尾交换,从而实现固定最大的数字。这样原本的堆再利用堆删除的思想,就可以保证堆的关系不会乱,时间复杂度此时变成了:建堆
O(NlogN)
,选数O(N-1)logN
,总的时间复杂度就是相加,就变成了O(NlogN)
,这个算法的量级也就下降了(时间复杂度是判定算法的量级,即使相同时间复杂度的算法,他们的量级也会有区别)
那还有更高效的建堆方法吗?答案就是直接利用向下调整算法!
从倒数第一个非叶子,即最后一个节点的父亲开始调整,这样可以减少一般的时间复杂度,而且调整的方式也会改变,从而使其更快
建堆的两个时间复杂度将在最后进行分析。
2)利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序,下面将根据上述的思路进行升序
3)实现堆排序(升序-大堆)
依托上面堆实现的代码,但要修改向上调整算法和向下调整算法的判定条件为大堆。
#include "Heap.h"
void HeapSort(int* a, int n)
{
// 建大堆
把a[0]当作一个大堆,其他数据插入
O(NlogN)
//for (int i = 1; i < n; i++)
//{
// // 修改判定条件为大堆,调整符号
// AdjustUp(a, i);
//}
// 只利用向下调整算法,效果更高,时间复杂度是 O(N)
// 从倒数第一个非叶子,即最后一个节点的父亲
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// 升序使用向下调整算法,用堆删除的思路调整堆
// O(NlogN)
int end = n - 1;
while (end > 0)
{
// 先排序大的数据,大的往后面先排好序
Swap(&a[0], &a[end]);
// 修改判定条件为大堆,然后调整堆
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
int a[] = { 2,7,0,21,56,786,1,3 };
HeapSort(a, sizeof(a) / sizeof(int));
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
4)运行效果
5)分析建堆的时间复杂度
首先要知道满二叉树有以下结论:
2^h-1 -> h=log(N+1)
对于建堆的两种办法:
一)向上调整算法【最终近似O(NlogN)】
二)向下调整算法【最终近似O(N)】
三)选数字排序【O(NlogN)】
2. TOP-K问题(小堆)
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中),因此最佳的方式就是用堆来解决
1)基本思路
思路1: N个数插入大堆里面, Pop k次,时间复杂度是 O(N*logN+K*logN) -> O(N*logN)
,当N远大于k,且N很大时,空间占用会很高,内存不够用,因此方案不太行
思路2: 首先读取前k个值,建立k个数的小堆,再一次读取后面的值,跟堆顶比较;如果比堆顶大,替换堆顶进展,再向下调整。此时的时间复杂度就变成了 O(N*logK)
下面将模拟实现思路2的场景:10亿个随机数字找出最大的前10个。首先向文件写入随机数字,用 fprint
写入文件,再用 fscanf
读取建立小堆,之后开始遍历。为了检验是否有错误,手动进入文件修改5个数字,使其大于10000000000(一百亿),如果5个数都大于10000000000,就证明思路实现正确。
2)实现代码,要建立小堆
#include "Heap.h"
void CreateNDate()
{
// 造数据
int n = 10000000000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
// +i可以防止过多数据重复
int x = (rand() + i) % 10000000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void PrintTopK(const char* file, int k)
{
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen fail\n");
exit(-1);
}
// 要先对向上/向下调整算法判定条件修改
// 开辟小堆空间
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail\n");
exit(-1);
}
// 读取前k个数建一个k个数小堆
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
AdjustUp(minHeap, i);
}
// 找大的数字进堆
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
if (x > minHeap[0])
{
minHeap[0] = x;
AdjustDown(minHeap, k, 0);
}
}
// 打印
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
printf("\n");
// 使用完就释放空间
free(minHeap);
minHeap = NULL;
// 要记得关闭文件
fclose(fout);
}
int main()
{
// 一百亿个数据要跑很久,可以将数据量减少
CreateNDate();
PrintTopK("data.txt", 6);
return 0;
}
3)运行效果
以上就是堆的全部内容啦!!