📖 前言:本期我们将详解堆相关的应用,如堆排序,Top-K问题
🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:
目录📌
🕒 1. 堆的应用
🕘 1.1 堆排序
堆排序相关知识点:
-
选择排序的一种
-
是利用
堆
这种数据结构的思想进行排序
💡 思路1:插入 - 删除堆
假如排升序,我们利用小堆进行排序的话:
我们建好一个N个数的小堆后,堆顶肯定是最小值,把这个最小值放到数组里,然后pop一下栈顶,再取下一个次小值,重复上述步骤即可。
void HeapSort(int* a, int n)
{
HP hp;
HeapInit(&hp);
for (int i = 0; i < n; i++)
{
HeapPush(&hp, a[i]);
}
for (int i = 0; i < n; i++)
{
a[i] = HeapTop(&hp);
HeapPop(&hp);
}
HeapDestroy(&hp);
}
这样有什么问题吗?
空间复杂度为O(N),因为你又开辟了一个小堆,并且你这样为了排序,还得写一个堆实现,这里能不能有更优的方法呢?
💡 思路2:模拟插入 - 向上/下调整建堆 - 依次选数调整
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
我们可以把数组直接当成一个堆,以小堆为例,这里排的是升序,由于小堆的堆顶元素是最小值,这样我们就选出来最小值了,那怎么选出次小值呢?
我们只能把最小的那个值放到数组首,然后剩下的再看成一个堆
但是这样的问题很明显,我们之前建立好的小堆被打乱了,我们现在只能把剩下的再重新建小堆,这时的时间复杂度变成了O(N2) ,还不如直接遍历选数。
所以我们不妨来试试排升序建大堆
建大堆后,堆顶是最大值,然后让堆顶和最后的交换,向下调整,这样最大的值就被放到最后了,然后最后一个数不看成堆里面的元素,现在堆顶又是次大值了,再和后面交换,向下调整。以此类推最后就排序好了。
我们用向下调整法建堆,上面说过了,建堆的时间复杂度为O(N)
然后再选堆顶的数放后面,向下调整,每一次时间复杂度为O(logN)
总共N个数,所以是NlogN
所以最终的堆排序的时间复杂度为O(NlogN)。
大堆排升序动画:
升序:建大堆
降序:建小堆
void HeapSort2(int* a, int n)
{
//建堆 --- 时间复杂度:O(N)
//大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
ADjustDown(a, n, i);
}
int end = n - 1;
while (end > 0) //end = 0的时候就截止:即 长度为0就截止
{
Swap(&a[0], &a[end]);
//选出次大值
ADjustDown(a, end, 0);
end--;
}
}
🕘 1.2 Top-K问题
问题大意:在N个数中找出最大/小的前K个数(一般K远小于N)
比如:在高考出成绩时,需要对全省考生的成绩进行排序并取出前50名考生的成绩进行屏蔽,此时就涉及Top-K问题了
接下来以找最大的前k个数举例
💡 思路1:堆排序
将所有数据进行排序,取前K个元素即可
void PrintTopKSort(int *a,int n,int k)
{
HeapSort(a,n);
for(int i=0;i<k;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
时间复杂度是O(N*logN)
💡 思路2:将N个数建堆,取出前K个
建好堆后,将堆顶的数与最后一个数交换位置,向下调整一次找次大的数(注意向下调整时不要包含交换后的最后一个位置),重复这样的操作,直到找完K个数
注意:
需要前K个最大
的数时,建小堆
需要前K个最小
的数时,建大堆
void PrintTopKBuildN(int* a, int n, int k)
{
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
for (int i = 0; i < n-k; i++)
{
Swap(&a[0], &a[end]);//交换最值和最后一个位置的数
end--;//不包含最后一个位置
AdjustDown(a, end, 0);//调整
}
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
N个数建堆时间复杂度是O(N)
,Pop K次的时间复杂度O(KlogN)
总时间复杂度为O(N+KlogN)
上面两种方法是有缺点的,假设N非常大,N是100亿(约40G),堆是在内存操作的,内存就存不下这些数据,他们只能存在文件中。
💡 思路3:建前K个数的堆(最优)
- 用前
K
个数建立一个K个数的小堆 - 依次遍历后续
N-K
个数,比堆顶的数据大,就替换堆顶数据,向下调整进堆。 - 然后再插入新数(因为是降序找最大的K个数,我们建的是小堆,所以堆顶是最小的数,当有比它大的数就替换它进堆)
- 最后堆里的K个数就是最大的前K个数
void PrintTopKBuildK(int* a, int n, int k)
{
//建k个数的堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)//O(k)
{
AdjustDown(a, k, i);//取最大的需要建小堆,取最小的需要建大堆
}
for (int i = k; i < n; i++)//O(n*logk)
{
if (a[0] < a[i])//比堆顶大的就入堆
{
Swap(&a[0], &a[i]);
AdjustDown(a, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
建立一个K
个数的堆时间复杂度是O(K)
剩下N-K
个数向下调整时间复杂度O( (N-K)*logK )
总时间复杂度为O(K+(N-K)*logK)≈O(N)
💡 思路4:建前K个数的堆(文件版)
void CreateDataFile(const char* filename, int N)
{
FILE* fin = fopen(filename, "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
srand(time(0));
for (int i = 0; i < N; ++i)
{
fprintf(fin, "%d\n", rand() % 1000000);
}
fclose(fin);
}
void PrintTopKByFile(const char* filename, int k)
{
assert(filename);
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
return;
}
// 如何读取前K个数据
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
}
// 建k个数小堆
for (int j = (k - 2) / 2; j >= 0; --j)
{
AdjustDown(minHeap, k, j);
}
// 继续读取后N-K
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, k, 0);
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
free(minHeap);
fclose(fout);
}
int main()
{
const char* filename = "Data.txt";
int N = 10000;
int K = 10;
CreateDataFile(filename, N);
PrintTopKByFile(filename, K);
return 0;
}
OK,以上就是本期知识点“堆的应用”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~