建堆、堆排序、TopK问题大合集

article/2023/6/4 14:56:05

一、如何建堆

1、向上调整建堆法O(NlogN)

原理:

利用向上调整的方法进行建堆,是通过模仿之前堆的插入操作,从第二个数开始,每次插入一个数,就对这个数进行向上调整,这样子既保证了原有数据为堆(即满足向上调整的条件),又保证了插入一个数据后不会破坏这个堆。这里以建大堆为例

实现逻辑:

传入孩子位置的下标,通过parent = (child-1)/ 2找到parent的下标,比较父亲和孩子的val值的大小,孩子大则交换父子,然后child变成parent,parent继续向上找parent,直到孩子变成下标为0的元素,即最大元素时停止,或者当孩子小于父亲时直接跳出。

图示:

 对于一组无序的数字  2  1  5  7  6  8  0  9  4  3  ,从i=1开始,即下标为1的第二个元素开始,模拟插入的过程。

对于第二个数据,插入前原来只有一个元素,可看作堆,放入后进行向上调整,调整完后是一个有2个元素的堆。

 插入1后不用调整即为大堆。

对于第三个数据,插入前是2个元素的堆,插入后向上调整,变成有3个元素的堆。

 调整完第四个

调整完所有元素为  9   8   7   6   5   2   0   1   4   3

 

如图所示,就得到了一个大堆。

由于每插入一个元素,都要向上调整h=logN,所以时间复杂度为O(NlogN) 

2、向下调整建堆法O(N)

前提:使用向下调整的前提是左右子树必须已经是堆的结构了,因此不能从堆顶下标为0的位置开始调整,否则会导致父子关系错乱,最后得到的结果不是堆。

原理:

对于这样一组数据,我们该怎样使用向下调整法呢?

对于叶子节点,它们没有孩子,因此没有向下调整的必要。

对于第一个非叶子节点6,它有孩子为3,且单纯的一个3可以看作是大堆,因此可以从6开始向下调整,但是6>3因此调整后的结果不变。然后对于7这个结点,左孩子9可以调整,且左右孩子均可看作是左右子树,可以向下调整。

 对于下一个结点5,左右子树8和0均可看作大堆,满足向下调整条件。

然后对于结点1,它的左右子树都是真的大堆,可以向下调整。

最后对于堆顶的结点2,左右子树均为大堆,继续调整。

 

 从第一个非叶子节点开始,位置是(n-1-1)/2,第一个减一找到范围内的第一个孩子,然后-1除2找到其父亲,即第一个非叶子节点。

对于向下调整函数内部,传入要向下调整的位置,先找到其父亲,调整范围为size个数据,child作为下标不能>=size,如果孩子比父亲大就交换,然后parent变为child,child继续找下一个child,直到child>=size,其中不满足就跳出。

同时,为了得到左孩子和右孩子的大小加一步判断,并且要防止右孩子越界,即child+1<size。

向下调整建堆的时间复杂度为O(N)。看似是N个数据,每个都是h=logN趟,但经过计算后为O(N)。对比向上建堆,上层数据个数少,调整的步数多,下层数据多,但调整的步数少,因此总的调整次数就比向上建堆少。(这是一种巧记方法) 

二、堆排序

升序  --  建大堆

降序  --  建小堆

以升序为例。如果建小堆,则只能保证堆顶的元素为最小的,满足升序,如果此时看剩下的n-1个元素,把它们看作新的堆,就会使父子关系错乱,从而不满足堆的结构。

而参照堆中pop函数的实现,我们可以先建一个大堆。

建好后将堆顶元素与最后一个元素交换,然后保证原堆顶的数在数组尾部不动,新堆顶向下调整,此时堆的范围虽然也是n-1,但是包含的是第二大到最小的元素,只有堆顶的元素变化,其它元素没有变化,堆顶的左右子树仍然满足大堆(最后一个数据不在范围内)。

然后经过向下调整,既把最大的元素放到数组尾,又将剩余元素形成了一个新堆,因此可以循环起来,不断将堆顶元素放到数组尾,最终完成排序。

 

先利用向下调整法建堆。

传入要排序的数据个数sz,end=sz-1指向最后一个元素,交换堆顶后最后一个元素,对剩下的sz-1个,即end个元素范围内的数据,对堆顶的元素向下调整。end为1时,为1 -  0的大堆,交换一下变为0-1,对于0这一个元素,向下调整不变。 

堆排序对n个元素分别向下调整,时间复杂度为O(NlogN)。

最下面一层,包含约一半的结点,都是从堆顶向下调整得到的,因此也是N*logN,与向上调整类似(巧记)。

三、堆的应用

TOP-K问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。

例如:有100亿个整型,占40个GB(实际上37多一点),而电脑的内存可能只有4-8G,无法一下存储这么多数据,后续也就不能进行访问、排序操作。

当内存存不下时,数据就存在磁盘文件中,但磁盘文件速度慢,且不能随机访问,因此不能使用简单的暴力解法。

最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

堆顶的数据一定是50个数据中的最大或最小,也就是Top50(以取最大的k个元素为例),外部的数如果比他大,说明已经有50个元素比堆顶元素大了,堆顶元素最大只能是top51,那么就用此元素替换堆顶元素,然后向下调整。

        向下调整的目的是保证堆顶的元素是50个中最小的,方便后续比较。如果不是最小(建大堆时),假设是所有数据中最大的元素,则其它所有元素都无法进入,最初的50个元素就被认为是top50了,显然,这是错误的。因此TOP-K-MAX--建小堆,反之建大堆。

        实际上,堆的结构有2个好处。

1、堆顶的元素是堆中k个元素的 最大/最小,选出topk时,只需要将堆顶的第50名与其它元素比较,符合则进入前50,反之继续下一个元素。

2、堆的内部结构,保证了父亲大于/小于  左右子树,因此遍历(调整)时可以依靠大小关系,将O(N)简化为O(logN),提高了效率。

 这里我rand了10000以内的10000个值,为了标记我们找到的是前5个最大的值,我将其中5个的值设置为超过10000。

函数内,先建立k个元素的小堆,然后遍历剩下n-k个元素,大于则交换,然后对堆顶元素向下调整,最后得到topk个最大值。

http://www.ngui.cc/article/show-1007681.html

相关文章

测试开发进阶系列课程

测试开发系列课程1.完善程序思维--------案列&#xff1a;图书管理系统的创建**&#xff08;一&#xff09;图书管理系统的创建**1.完善程序思维--------案列&#xff1a;图书管理系统的创建 &#xff08;一&#xff09;图书管理系统的创建 1.在main中写入主函数&#xff0c;…

数位DP算法学习总结

一、数位dp简述模板数位dp是一种计数时使用的动态规划算法&#xff0c;一般是要统计一个区间 [left, right] 内符合给定条件数字的个数&#xff0c;例如HDU 2089 不要62中的统计给定区间内不包含4以及62数字的个数&#xff0c;数位dp其实是暴力枚举算法的优化&#xff0c;通过过…

添加Anaconda Powershell Prompt到右键

想要使用Anaconda Powershell Prompt每次还要去开始菜单打开&#xff0c;而且还要切换到特定目录下&#xff0c;十分麻烦。通过将Anaconda Powershell Prompt添加到鼠标右键&#xff0c;可在当前目录十分方便的打开Anaconda Powershell Prompt。步骤如下&#xff1a; 1. 首先开…

Java_Spring:4. 使用 spring 的 IoC 的实现CRUD【案例】

目录 1 需求和技术要求 1.1 需求 1.2 技术要求 2 环境搭建 2.1 拷贝 jar 包 2.2 创建数据库和编写实体类 2.3 编写持久层代码 2.4 编写业务层代码 2.5 创建并编写配置文件 3 配置步骤 4 测试案例 4.1 测试类代码 4.2 分析测试了中的问题 1 需求和技术要求 1.1 需求…

Spring - Spring 注解相关面试题总结

文章目录01. Spring 配置方式有几种&#xff1f;02. Spring 如何实现基于xml的配置方式&#xff1f;03. Spring 如何实现基于注解的配置&#xff1f;04. Spring 如何基于注解配置bean的作用范围&#xff1f;05. Spring Component, Controller, Repository, Service 注解有何区别…

2023-3-25 java选择题每日一练

继承中类, 静态代码块, 实例代码块和构造方法的执行顺序其原理如下:当没有子类继承的时候顺序&#xff1a;静态代码块 → main → 构造代码块 → 构造方法public class Test {static{System.out.println("父类静态代码块开始执行&#xff01;");}{System.out.println…

【WMS学习】从悬浮窗的添加来看窗口的add和update

这里我们从一个悬浮窗应用来查看WindowManager的addView使用&#xff0c;从这里作为突破口来认识窗口的添加&#xff0c;和窗口的位置大小更新方法updateViewLayout&#xff0c;使用WindowManager的addView方法来添加窗口非常的直观&#xff0c;因为Activity的显示中&#xff0…

领域驱动设计(Domain-Driven Design, DDD)

领域驱动设计&#xff08;Domain Driven Design&#xff0c;简称DDD&#xff09;是一种面向对象软件开发方法&#xff0c;它强调将软件系统的设计和实现过程与业务领域紧密结合&#xff0c;通过深入理解和建模业务领域&#xff0c;从而达到高内聚、低耦合的目的。 领域驱动设计…

【ChatGPT】比尔·盖茨最新分享:ChatGPT的发展,不止于此

✅作者简介&#xff1a;在读博士&#xff0c;伪程序媛&#xff0c;人工智能领域学习者&#xff0c;深耕机器学习&#xff0c;交叉学科实践者&#xff0c;周更前沿文章解读&#xff0c;提供科研小工具&#xff0c;分享科研经验&#xff0c;欢迎交流&#xff01;&#x1f4cc;个人…

【学习总结】IMU噪声的连续形式与离散形式

乱七八糟的&#xff0c;查了半天资料&#xff0c;整理如下。 &#xff08;网上其他地方的资料也很混乱&#xff0c;这篇总结是我综合比对&#xff0c;得出的结论&#xff09; 统一符号 连续形式&#xff1a; gyroscope white noise: σg\sigma_gσg​ accelerator white nois…