目录
🍎前言🍎:
🥝一、TOP-K 问题概述🥝:
🍉二、不同解决思路实现🍉:
①排序法:
②直接建堆法:
③K 堆法(最优解):
🍒总结🍒:
🛰️博客主页:✈️銮同学的干货分享基地
🛰️欢迎关注:👍点赞🙌收藏✍️留言
🛰️系列专栏:🎈 数据结构
🎈【进阶】C语言学习
🎈 C语言学习
🛰️代码仓库:🎉数据结构仓库
🎉VS2022_C语言仓库
家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
🏡🏡 本文重点 🏡🏡:
🚅 堆的 TOP-K 问题 🚏🚏
🍎前言🍎:
在上节课中我们已经学习了二叉树的顺序存储结构,并且对于实际使用中所常用的顺序存储结构——堆的各个接口功能进行了理解与实现,而这节课我们将要对堆的实际应用进行更加深入的研究,而关于堆最重要的实际应用,就是用于处理 TOP-K 问题。
🥝一、TOP-K 问题概述🥝:
TOP-K 问题,即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量较大。 比如:年级前10名、世界百强、游戏中活跃前百玩家等等。
而对于 Top-K 问题,我们能想到的有三种不同的思路去解决。首先最简单直接的方式就是排序。但是如果需要处理的数据量非常大,排序就不太可取了(数据难以瞬时全部加载到内存中)。而另外两种方式就是使用堆来解决:
- 用数据集合中前 K个元素来建堆,若要取前 K 个最大的元素,则建小堆;而若要取前 K 个最小的元素,则建大堆。
- 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素,最终将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。
🍉二、不同解决思路实现🍉:
关于这一部分的研究就是本文的重点介绍内容,即关于堆的应用 ——TOP-K 问题的三种解决思路的具体实现:
①排序法:
排序法的思路很好理解,是将所有的数据进行排序,再根据需求取值即可。过程中使用的排序方法是向下调整算法(在上节课中十分详细的为各位小伙伴们尽行了讲解,这里不再作过多阐述),时间复杂度是O(nlogn):
//排序法:
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整算法:
void ADjustDown(HPDataType* data, HPDataType father, int size)
{
//方法一:迭代
//HPDataType child = father * 2 + 1;
// //if ((data[child] < data[child + 1])&&(child+1<size)) // 找到孩子较大的一个
//if ((data[child] > data[child + 1]) && (child + 1 < size)) // 小堆,找到孩子较小的一个
//{
// child++;
//}
//while (child<size)
//{
// if (data[father] < data[child]) // 大堆
// //if (data[father] > data[child]) // 小堆
//
// {
// Swap(&data[father], &data[child]);
// father = child; // 孩子变父亲,向下迭代
// child = father * 2 + 1;
// }
// else
// {
// break;
// }
//}
//方法二:递归
HPDataType child = father * 2 + 1;
if (child >= size)return;
else
{
//if ((data[child] < data[child + 1]) && (child + 1 < size)) // 大堆,找到孩子较大的一个
if ((data[child] > data[child + 1]) && (child + 1 < size)) // 小堆,找到孩子较小的一个
{
child++;
}
//if (data[father] < data[child]) // 大堆
if (data[father] > data[child]) // 小堆
{
Swap(&data[father], &data[child]);
}
ADjustDown(data, child, size);
}
}
void Heapsort(HPDataType* data, int size)
{
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
ADjustDown(data, i, size);
}
int end = size - 1;
while (end > 0)
{
Swap(&data[0], &data[end]);
ADjustDown(data, 0, end);
end--;
}
}
这种方法就相当于遍历所有的数据进行比较排序,因此就将会造成大量的内存消耗和使用,存在着较大的弊端。
②直接建堆法:
直接建堆法的作用原理为:建立一个大堆(时间复杂度O(logn)),然后取出堆顶的元素并将其删除,再重复这个过程 K 次,就能得到我们想要的结果,这个过程中同样使用了向下调整算法(对这个算法还不太理解的小伙伴们可以回到上节课中进行一下快速的复习):
//直接建堆法:
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
ADjustDown(data,i,size);
}
for(int i=0;i<k;i++)
{
printf("%d ",HeapTop(data);
HeapPop(data);
}
虽然这种算法有了一定程度上的改进,但是仍没有改变在内存中进行操作的本质,于是虽然也可以实现求取比较结果的目的,但其操作方式导致其仍会造成大量的内存占用与消耗,仍没有达到理想的状态,需要对相关算法继续进行改进。
③K 堆法(最优解):
我们不难发现,上述两种方法都是在内存中执行的,于是当我们 n 很大时,所占用内存将会非常大,例如我们假设 n 为100亿,此时就有:
1G = 1024 MB = 1024 * 1024KB = 1024 * 1024 * 1024Byte ,则需要使用的内存就将达到恐怖的 10亿Byte 左右。
而我们也知道,在我们的实际使用中,很少会有这么大的内存空间,而就算有那么大的内存,那么这中间消耗的成本也将是天文数字,并且我们付出如此巨大的代价,只求得了数据结合中前 K 个最大的元素或者最小的元素,如此看来得不偿失。
于是我们就采用另一种建堆方式——K 堆法:建一个大小为 K 的小堆(小根堆)。为什么是小堆呢,我们知道小堆是用来排升序的,当我们向后遍历数据和堆顶比较时,若比堆顶大就替换,然后继续向下调整,通过这样的方式就可以减少内存的使用。
这里可能有小伙伴会问了,那假如一开始那个数据是 n ,那么需要的内存不也十分夸张吗?关于这个问题,原因在于我们在实际生活中的使用中,数据不一定是来自内存,更多的是来自硬盘、数据库或者网络,并不会大量的消耗和使用我们的内存空间。并且反观上述两种方法,都是将数据储存在内存上,如果我们使用归并排序,但是数据不在内存上而是储存在硬盘等地方,此时归并排序的执行效率也会大大降低:
//K堆法:
//向下调整算法:
void ADjustDown(HPDataType* data, HPDataType father, int size)
{
//方法一:迭代
//HPDataType child = father * 2 + 1;
// //if ((data[child] < data[child + 1])&&(child+1<size))//找到孩子较大的一个
//if ((data[child] > data[child + 1]) && (child + 1 < size))//小堆,找到孩子较小的一个
//{
// child++;
//}
//while (child<size)
//{
// if (data[father] < data[child])//大堆
// //if (data[father] > data[child])//小堆
//
// {
// Swap(&data[father], &data[child]);
// father = child;//孩子变父亲,向下迭代
// child = father * 2 + 1;
// }
// else
// {
// break;
// }
//}
//方法二:递归
HPDataType child = father * 2 + 1;
if (child >= size)return;
else
{
//if ((data[child] < data[child + 1]) && (child + 1 < size))//大堆,找到孩子较大的一个
if ((data[child] > data[child + 1]) && (child + 1 < size))//小堆,找到孩子较小的一个
{
child++;
}
//if (data[father] < data[child])//大堆
if (data[father] > data[child])//小堆
{
Swap(&data[father], &data[child]);
}
ADjustDown(data, child, size);
}
}
void PrintTopK(int* a, int n, int k)
{
HPDataType* kMinHeap = (HPDataType*)malloc(sizeof(HPDataType) * k);
assert(kMinHeap);
for (int i = 0; i < k; i++)
{
kMinHeap[i] = a[i];
}
for (int i = (k - 2) / 2; i >= 0; i--)
{
ADjustDown(kMinHeap, i, k);
}
for (int j = k; j < n; j++)
{
if (kMinHeap[0] < a[j])
{
kMinHeap[0] = a[j];
ADjustDown(kMinHeap, 0, k);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", kMinHeap[i]);
}
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
int main()
{
TestTopk();
return 0;
}
这种方式也同样使用了向下调整算法,由此可见,该算法的重要程度可见一斑。通过使用 K 堆法,我们就大幅度的节省了内存空间的消耗,同时也保证了程序的执行效率,是现阶段我们能力范围内所能实现的最优解。
🍒总结🍒:
到这里,我们今天关于 TOP-K 问题的研究就全部结束了,这个问题将在我们以后的工作过程中大量遇到,希望各位小伙伴们们能够结合实际情况,选择最合适的方式去解决、处理类似的问题。同时,向下调整算法与向上调整算法的思想也是很重要的一部分知识结构,希望还没能将其牢固掌握、熟练应用的小伙伴们下去以后能够把这部分知识再次认真全面的进行复习,为以后的学习和使用打下坚实的基础。
🔥🔥精彩的人生是在有限生命中实现无限价值的人生🔥🔥
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!