【数据结构入门】堆的应用:TopK 堆排序

news/2024/5/19 6:24:48 标签: 数据结构, 算法, c语言, 堆排序, topk

文章目录

      • 一、Top-k问题
        • 1.1 解法一:暴力排序
        • 1.2 解法二:建N个数的堆
        • 1.3 解法三:建K个数的堆(最优解法)
      • 二、堆排序

一、Top-k问题

Top-k问题:在 N 个数中,找出前 K 个(最大/最小)的元素,一般情况下数据量 N 都远大于 k。

Top-k问题在生活中是非常的常见,比如游戏中某个大区某个英雄熟练度最高的前10个玩家的排名,我们就要根据每个玩家对该英雄的熟练度进行排序,可能有200万个玩家,但我只想选出前10个,要对所有人去排个序吗?显然没这个必要。

再比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

1.1 解法一:暴力排序

对于Top-K问题,首先想到的最简单直接的方式就是排序。

我们用堆排序,其时间复杂度为:O(N*log2N)。

但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。


1.2 解法二:建N个数的堆

建一个 N 个数的堆(C++中可用优先级队列priority_queue),不断的选数,选出前 k 个。

时间复杂度:建N个数的堆为O(N),获取堆顶元素 (也即是最值) 并删除掉堆顶元素为O(log2N),上述操作重复 k 次,所以时间复杂度为O(N+k*log2N)。

思考

能否再优化一下呢?假设 N 是 10 亿数,内存中放不下,是放在文件中的。前面两个方法都不能用了。


1.3 解法三:建K个数的堆(最优解法)

基本思路如下:

  1. 用数据集合中前K个元素来建堆。

    找前 k 个最大的元素,则建小堆

    找前 k 个最小的元素,则建大堆

  2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则删除堆顶元素,再插入。

    找前 k 个最大的元素,大于堆顶元素,则删除堆顶元素,再插入

    找前 k 个最小的元素,小于堆顶元素,则删除堆顶元素,再插入

  3. 将剩余的 N-K 个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

时间复杂度

  • 建 k 个元素的堆为O(K);
  • 遍历剩余的 N-K 个元素的时间代价为O(N-K),假设运气很差,每次遍历都入堆调整;
  • 入堆调整:删除堆顶元素和插入元素都为O(log2K);
  • 所以时间复杂度为O(k + (N-K)log2K)。当 N 远大于 K 时,为O(N*log2K),这种解法更优。

假如要找出最大的前 10 个数

  • 建立 10 个元素的小堆,数据集合中前 10 个元素依次放入小堆,此时的堆顶元素是堆中最小的元素,也是堆里面第 10 个最小的元素,
  • 然后把数据集合中剩下的元素与堆顶比较,若大于堆顶则去掉堆顶,再将其插入,
  • 这样一来,堆里面存放的就是数据集合中的前 10 个最大元素,
  • 此时小堆的堆顶元素也就是堆中的第 10 个最大的元素

思考】为什么找出最大的前10个数,不能建大堆呢?
如果你建的10个元素的大堆,堆顶元素恰好是数据集合中最大的那个,那第2大的数、第3大的数就不能找不到了。

找出最大的前 10 个数代码如下

void PrintTopK(int* a, int n, int k)
{
	// 1. 建堆, 用a中前k个元素建堆
	Heap hp;
	HeapInit(&hp, a, k); // 建小堆

	// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	for (int i = k; i < n; i++)
	{
		if (a[i] > HeapTop(&hp)) // 如果大于堆顶元素
		{
			HeapPop(&hp); // 删除掉堆顶元素
			HeapPush(&hp, a[i]); // 插入该元素
		}
	}

	// 3. 打印前k个元素
	HeapPrint(&hp);

    // 4. 销毁堆
	HeapDestroy(&hp);
}

void TestTopk()
{
    // 用10000个数据集合来测试
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		perror("malloc");
		exit(-1);
	}

	// srand()函数 -- 设置一个随机的起点
	// 在随机数生成器中植入当前时间,这样rand()每次运行的数字都会不同
	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);
}

二、堆排序

上一篇博客已写过。


http://www.niftyadmin.cn/n/1579697.html

相关文章

【数据结构入门】链式二叉树的遍历及相关接口的实现

文章目录前言一、二叉树的链式结构二、二叉树的遍历方式1.1 遍历方式的规则1.2 前序遍历1.3 中序遍历1.4 后序遍历1.5 层序遍历三、二叉树的相关接口实现3.1 二叉树节点个数3.2 二叉树叶子节点个数3.3 二叉树第 k 层节点个数3.4 二叉树的深度(高度)3.5 二叉树查找值为 x 的节点…

Oracle中job的使用

2019独角兽企业重金招聘Python工程师标准>>> 昨日刚了解的Oracle中job是怎么回事&#xff0c;做个简单的笔记&#xff0c;有什么不正确的地方还请各位大拿指教&#xff1b;当我们需要定时在后台执行相关操作:如每天晚上0点将一张表的数据保存到另一张表中或者定时备…

【OJ - 二叉树】单值二叉树

【OJ - 二叉树】单值二叉树 文章目录一、题目描述二、解题思路LeetCode链接&#xff1a; 965. 单值二叉树 - 力扣&#xff08;LeetCode&#xff09;题目难度&#xff1a;简单一、题目描述 如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是 单值 二叉树。 只有给…

C#获取类名为Internet_Explorer_Server控件的内容

这篇文章的重点是讲解如何获取类名为Internet_Explorer_Server控件的内容 为了让大家都能够使用demo,我以IE为测试对象&#xff0c;另外为了突出重点&#xff0c;所以如何获取窗口句柄我就不做演示了(不清楚的童鞋,可以去Google下哈)&#xff0c;句柄值我使用spy获得 大家可以下…

引用 Ucenter通信原理总结

2019独角兽企业重金招聘Python工程师标准>>> <?php define(UC_CONNECT, mysql); // 连接 UCenter 的方式: mysql/NULL, 默认为空时为 fscoketopen()// mysql 是直接连接的数据库, 为了效率, 建议采用 mysql //数据库相关 (mysql 连接时, 并且没有设置 UC_DBL…

【OJ - 二叉树】二叉树的前、中、后序遍历

文章目录一、二叉树的前序遍历1.1 题目描述1.2 解题思路二、二叉树的中序遍历2.1 题目描述2.2 解题思路三、二叉树的后序遍历3.1 题目描述3.2 解题思路这三道OJ题解题思路类似&#xff0c;你只要会其中一道&#xff0c;其它两道也就会了一、二叉树的前序遍历 题目难度&#xf…

纽约时报:大数据技术存在局限 直觉不可或缺

转载自&#xff1a;http://tech.sina.com.cn/it/2012-12-31/22177940388.shtml?bsh_bid176656678 导语&#xff1a;《纽约时报》印刷版30日出版文章称&#xff0c;大数据将成为人类商业历史上新的篇章&#xff0c;有望取代想法、范例、组织以及人们思考世界的方式。但与此同时…

【OJ - 二叉树】检查两棵树是否相同

文章目录一、题目描述二、解题思路题目难度&#xff1a;简单一、题目描述 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 LeetCode链接&#x…