十四、堆(Heap)
0、
堆(Heap)
堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法。
引入:快速排序平均情况下,时间复杂度为O(nlogn),甚至堆排序比快速排序的时间复杂度还要稳定,但在实际中,快排性能要比堆排序好,为什么???
一、堆的概述
堆——是一种特殊的树
- 堆是一个完全二叉树(除了最后一层,其他层的节点个数都是满的,最后一层的节点都是靠左排列)
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值(也就是说,堆中每个节点的值都大于等于(或小于等于)其左右子节点的值)
大顶堆:对于每个节点的值都大于等于子树中每个节点的堆。
小顶堆:对于每个节点的值都小于等于子树中每个节点的堆。
注意:对于同一组数据,可以构建多种不同形态的堆(大顶堆可有不同形态,小顶堆同理)
二、堆的操作
ps:以大顶堆为例
1、插入操作
- 把新插入的元素放在堆的最后;
- 堆化(heapify):对堆进行调整,使其重新满足堆的特性
- 两种堆化方法:
- 思路:顺着节点所在的路径,向上或向下进行对比,然后交换
- (1)从下往上(如下图所示)
- (2)从上往下
- 两种堆化方法:
==》code
public class Heap {private int[] a; // 数组,从下标1开始存储堆中的数据private int n; // 堆可以存储的最大数据个数private int count; // 堆中已经存储的数据个数// 初始化public Heap(int capacity) {a = new int[capacity + 1];n = capacity;count = 0;}public void insert(int data) {if(count >= n)return; // 堆满了count++;a[count] = data;int i = count;while(i/2 > 0 && a[i] > a[i/2]) {// 自下往上堆化// swap()函数用于交换下标为 i 和 i/2 的两个元素swap(a, i, i/2);i = i/2;}}
}
2、删除堆顶元素
栈顶元素存储的就是堆中数据的最大值或最小值。
以大顶堆为例,堆顶元素就是最大元素。当删除堆顶元素之后,则需要把第二大的元素放到堆顶,第二大元素必为于左右子节点中。然后迭代地删除第二大节点,以此类推,直到叶子节点被删除。
==》会出现数组空洞,也就是堆化出来的堆并不满足完全二叉树的特性。
解决方法:
- 将最后一个元素放在堆顶;
- 然后利用同样的父子节点对比方法:若不满足父子节点大小关系,则交换两个节点并重复该过程,直到父子节点之间满足大小关系为止。==》从上往下的堆化方法
实现代码:
public void removeMax() {// 堆中没有数据if (count == 0) return -1;a[1] = a[count];--count;heapify(a, count, 1);
}private void heapify(int[] a, int n, int i) {// 自上往下堆化while(true) {int maxPos = i;// 寻找最大值的位置if(i*2 <= n && a[i] < a[i/2])maxPos = i * 2;if(i*2+1 <= n && a[maxPos] < a[i*2+1])maxPos = i * 2 + 1;if(maxPos == i)break;swap(a, i, maxPos);i = maxPos;}
}
3、时间复杂度分析
① 一个包含 n 个节点的完全二叉树,树的高度不会超过 log2n;
② 堆化过程是顺着节点所在路径比较交换的
==》堆化时间复杂度与树高成正比,也就是O(logn)
==》插入数据和删除堆顶元素主要逻辑是堆化
==》时间复杂度O(logn)
三、堆的存储
完全二叉树比较适合用数组来存储,非常节省存储空间。
- 原因:不需要存储左右节点的指针,可单纯地通过数组的下标,来找到节点的左右子节点和父节点。(根节点存储在数组下标为1的位置,数组下标为
i
的节点的左子结点的下标为2*i
,右子节点的下标为2*i+1
)
四、堆排序的实现
- 时间复杂度为O(n2):冒泡排序、插入排序、选择排序
- 时间复杂度为O(nlogn):归并排序、快速排序、线性排序
1、堆排序
堆排序:基于堆这种数据结构实现的排序算法。
堆排序时间复杂度非常稳定的原地排序算法——O(nlogn)
堆排序的过程大致分解为:建堆和排序
(1)建堆
目标:将数组原地建成一个堆。 原地就是不借助另一个数组,就在原数组上进行操作。
思路一:将元素依次插入堆中,数组下标从1开始。该思路从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化。
思路二
与思路一相反,从后往前处理数组,并且每个数据都是从上往下堆化。
思路二代码实现:
private static void buildHeap(int[] a, int n) {for(int i = n/2; i >= 1; --i){heapify(a, n, i); // 自上往下堆化}
}private void heapify(int[] a, int n, int i){// 自上往下堆化while(true){int maxPos = i;if(i*2 <= n && a[i] < a[i*2])maxPos = i*2;if(i*2+1 <= n && a[maxPos] < a[i*2+1])maxPos = i*2 + 1;if(maxPos == i)break;swap(a, i, maxPos);i = maxPos;}
}
分析:代码仅对下标从
n/2
开始到 1 的数据进行堆化,下标从n/2+1
到 n 的节点是叶子节点,不需要堆化时间复杂度:
- 节点堆化的时间复杂度:O(logn)
- n/2 个节点堆化的总时间复杂度:O(nlogn) ==》不够精确
- 堆排序的建堆过程的时间复杂度:O(n)
推导过程:
① 由于叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始;
② 每个节点堆化的过程中,需比较和交换的节点个数,与该节点的高度 k (从节点到叶节点的路径长度)成正比
③ 将每个非叶子节点的高度求和,公式如下:
④ 由于 h = log2n,代入公式S,可得 S = O(n)
⑤ 时间复杂度——O(n)
(2)排序——大顶堆《==》小顶堆
- 建堆后,数组中的数据已经是按照大顶堆的特性组织的;
- 将数组中的第一个元素(堆顶)与最后一个元素交换,则最大元素就放在下标为 n 的位置;
- 类似“删除堆顶元素”的操作,然后通过堆化方法将剩下的 n-1 个元素重新构建为堆;
- 重复上述过程,直到最后堆只剩一个下标为1的一个元素
代码
// n 表示数据的个数,数组 a 中的数据从下标 1 到 n 的位置。
public static void sort(int[] a, int n) {buildHeap(a, n);int k = n;while(k > 1){swap(a, 1, k);--k;heapify(a, k, 1);}
}
分析
- 原地排序算法:整个堆排序过程,都只需要极个别临时存储空间。
- 时间复杂度:建堆O(n)+排序O(nlogn)
- ==》整体时间复杂度: O(nlogn)
- 不稳定排序算法:在排序过程中,存在将堆的最后一个节点跟堆顶点互换的操作,可以改变值相同数据的原始相当顺序。
注意:若堆中数据从数组下标0开始存储,则节点下标为 i 时,其左子节点下标为 2i+1,右子节点的下标为2i+2,其父节点的下标为 (i-1)/2
五、在实际开发中,为什么快排要比堆排序性能好?
- 堆排序数据访问的方式没有快速排序友好。
- 快排数据——顺序访问
- 堆排序数据——跳着访问==》对CPU缓存不友好
- 对于同样的数据,在排序过程中,,堆排序算法的数据交换次数要多于快速排序。
- 快排数据交换的次数不会比逆序度多;
- 堆排序的建堆过程会打乱数据原有的相对前后顺序,导致原数据的有序度降低;
六、堆应用
场景:假设现有一个包含10亿个搜索关键词的日志文件,如何快速获取热门榜 Top 10 的搜索关键词?
思路:
- 通过哈希算法求取对应的哈希值,然后对哈希值同 10 取模,得到的结果就是这个搜索关键词应被分到的文件编码。
- 然后利用散列表和堆,分别求取 Top 10,将10个Top 10放在一起,取Top 10。
1、优先级队列
- 优先级队列:数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队。
- 堆可以看作为一个优先级队列——往优先级队列插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出最高的元素,就相当于取出堆顶元素。
- 应用:赫夫曼编码、图的最短路径、最小生成树算法等等。
- 实现:JAVA 中 PriorityQueue,C++的priority_queue等。
(1)合并有序小文件
目标:假设存在100个小文件,每个文件大小为100M,每个文件中存储都是有序的字符串。==》合并成一个有序的大文件。
思路1:类似归并排序中的合并函数。分别从100个文件中,各取第一个字符串,放入数组,然后比较,将最小的字符串放入合并后的大文件中,并从数组中删除;然后从最小字符串文件中取下一个字符串,并放入数组,重复上述过程,直到所有的文件中的数据都放入到大文件为止。
思路2:利用优先级队列,也就是利用堆。从小文件中取出字符串放入小顶堆中,那堆顶的元素就是优先级队列队首的元素,即最小的字符串;将该字符串放入大文件中,并将其从堆中删除;然后再从小文件中取出下一个字符串,放入到堆中,重复上述过程,直到可以将100个小文件中的数据依次放入大文件中。
==》删除堆顶数据和往堆中插入数据的时间复杂度:O(logn),n表示堆中的数据个数,这里是100
(2)高性能定时器
目标:有个定时器,定时器中维护很多定时任务,每个任务都设定了一个要触发执行的时间点。定时器每过一个很小的单位时间就要扫描一次任务,看是否有任务达到设定的执行时间,若达到,则执行。
思路:利用优先级队列按照设定的执行时间,将这些任务存储到优先级队列中,队列首部(堆顶)存储的就是最先执行的任务。
==》只需取队首任务的执行时间点,与当前时间相减,就得到一个时间间隔 T。
==》该时间间隔 T 就是从当前时间开始到第一个任务需要被执行的所需时间;从当前时间点到(T-1)这段时间内,定时器不需要做任何事情,当T时间过后,定时器取优先级队列中队首的任务执行。然后再计算新的队首任务的执行时间点与当前时间点的差值。
2、求 Top K
场景:将求 Top K 的问题抽象成两类。
- 针对静态数据集合——数据集合事先确定,不再改变;
- 针对动态数据集合——数据集合事先不确定,有数据动态地插入到集合中。
(1)静态数据
在包含n个数据的数组中,查找前 K 大数据
==》维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。若比堆顶元素大,则删除堆顶元素,将这个元素插入到堆中;若比堆顶元素小,则继续遍历数组。
==》 遍历数组的时间复杂度:O(n)
==》一次堆化操作的时间复杂度:O(logK)
==》最坏情况下,n个元素都入堆一次,时间复杂度:O(nlogK)
(2)动态数据
针对动态数据求得 Top K 就是实时 Top K。
一个数据集合中有两个操作:
- 添加数据:
- 询问当前的前 K 大数据:
- 维护一个大小为 K 的小顶堆,当有数据被添加到集合中时,将其与堆顶元素比较。若比堆顶元素大,则删除堆顶元素,将这个元素插入到堆中;若比堆顶元素小,则不做处理。
3、利用堆求中位数
目标:求动态数据集合中的中位数。
中位数:
- 若数据的个数是奇数,把数据从小往大排,第 n/2+1 个数据就是中位数;
- 若数据的个数是偶数,把数据从小往大排,第 n/2 个数据和第 n/2+1 个数据的单独一个或均值或…就是中位数(此处为第 n/2 个数据);
(1)利用堆高效地实现求中位数
通过维护两个堆:一个大顶堆,一个小顶堆。 大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中数据。
静态数据
如果有 n 个数据,n 是偶数,我们从小到大排序,那前 n/2 个数据存储在大顶堆中,后 n/2 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 n/2+1 个数据,小顶堆就存储 n/2 个数据。
动态数据
若新加入的数据小于等于大顶堆的堆顶元素,则将这个新数据插入到大顶堆;否则若新加入的数据大于等于小顶堆的堆顶元素 ,则将这个新数据插入到小顶堆。
若两个堆中的数据个数不符合前面约定的情况:
- 若 n 是偶数,则两个堆中数据个数都是 n/2;
- 若 n 为奇数,则大顶堆有 n/2+1 个数据,小顶堆有 n/2 个数据;
- 若两个堆数据个数不满足约定,则从一个堆中不停地将堆顶元素移动到另一个堆,通过这样的调整,让两个堆的数据满足上面约定。
==》插入数据需要堆化操作,所以时间复杂度:O(logn)
==》中位数只需返回大顶堆的堆顶元素,所以时间复杂度:O(1)
(2)利用堆求百分位的数据
目标:快速求接口的 99% 响应时间?
注:99 百分位数的概念可以类比中位数,如果将一组数据从
小到大排列,这个 99 百分位数就是大于前面 99% 数据。
如果有 n 个数据,将数据从小到大排列之后,99 百分位数大
约就是第 n*99%
个数据,同类,80 百分位数大约就是第 n*80%
个数据。
为了保持大顶堆中的数据占 99%,小顶堆中的数据占 1%,
在每次新插入数据之后,我们都要重新计算,这个时候大顶堆和小顶堆中的数据个数,是否还符合 99:1 这个比例。如果不符合,我们就将一个堆中的数据移动到另一个堆,直到满足这个比例。
十四、堆(Heap)相关推荐
- JVM上篇:内存与垃圾回收篇十四--垃圾回收器
JVM上篇:内存与垃圾回收篇十四–垃圾回收器 1. GC分类与新能指标 1.1 垃圾回收器概述 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商.不同版本的JVM来实现. 由于JDK的版本处于高 ...
- 第十四课 k8s源码学习和二次开发原理篇-调度器原理
第十四课 k8s源码学习和二次开发原理篇-调度器原理 tags: k8s 源码学习 categories: 源码学习 二次开发 文章目录 第十四课 k8s源码学习和二次开发原理篇-调度器原理 第一节 ...
- Frank Luna DirectX12阅读笔记:绘制进阶(第八章-第十四章)
目录 第八章 光照 8.1 光和材质的交互 8.2 法向 8.3 光照中其他重要的向量 8.4 Lambert余弦定律 8.5 散射光(diffuse lighting) 8.6 环境光(ambien ...
- 一文搞懂栈(stack)、堆(heap)、单片机裸机内存管理malloc
大家好,我是无际. 有一周没水文了,俗话说夜路走多了难免遇到鬼. 最近就被一个热心网友喷了. 说我的文章没啥营养,所以今天来一篇烧脑的. 哈哈,开个玩笑,不要脸就没人能把我绑架. 主要是最近研发第二代 ...
- 山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将...
山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将 "我是第一次--请你,请你温柔一点--"少女一边娇喘着,一边将稚嫩的红唇紧贴在男子耳边,樱桃小嘴盈溢 ...
- [Python从零到壹] 十四.机器学习之分类算法五万字总结全网首发(决策树、KNN、SVM、分类对比实验)
欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...
- 二叉树类图_数据结构(十四)——二叉树
数据结构(十四)--二叉树 一.二叉树简介 1.二叉树简介 二叉树是由n(n>=0)个结点组成的有序集合,集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的.互不相交的二叉树组成. ...
- 第十四章 - 垃圾回收概述
第十四章 - 垃圾回收概述 文章目录 第十四章 - 垃圾回收概述 1.什么是垃圾 1.1 **大厂面试题** 1.2 什么是垃圾? 2.为什么需要GC 3.早期垃圾回收 4.Java垃圾回收机制 担忧 ...
- 数据结构之堆(Heap)及其用途
本文采用图文码结合的方式介绍堆来实现优先队列 什么是优先队列? 队列是一种先进先出(FIFO)的数据结构.虽然,优先队列中含有队列两个字,但是,他一点也不像队列了.个人感觉,应该叫他优先群.怎么说那, ...
- 第十四届蓝桥杯大赛软件赛省赛JavaB组解析
目录 说在前面 试题 A: 阶乘求和 代码: 题目分析: 试题 B: 幸运数字 代码: 题目分析: 试题 D: 矩形总面积 代码: 题目分析: 试题 G: 买二赠一 代码: 题目分析: 试题 H: 合 ...
最新文章
- 在项目中常用到的几个注解@JsonInclude、@JsonFormat、@DateTimeFormat
- 计算机组装与维护 授课计划,计算机课程教学计划
- HDU 1879 继续畅通工程 最小生成树
- linux 多线程并行计算,浅谈.NET下的多线程和并行计算(五)线程池基础上
- Intellij IDEA 2017 如何导入 GitHub 中的项目
- 正经程序员是怎么完美度过元旦假期的?
- 我的Linux生涯之开机自动挂载
- 区块链的本质是什么?写给区块链的未来十年
- 【渝粤教育】电大中专成本会计作业 题库
- log4j使用和配置详解
- git本地项目推动到gitlab远端服务器
- 英特尔®以太网700系列的动态设备个性化
- 图片,PDF转换成文字
- 实现计算机系统的资源共享,实现多操作系统计算机的资源共享
- 使用R语言进行单(双)因素方差分析
- 两个日期区间跨度是否超过一年,开始日期距当前日期是否超过一年——js实现
- Unity - 人物对象的 LOD 管理
- 高德地图的circle圈
- Windows添加route
- c++由动态库dll文件生成lib文件的方法
热门文章
- rocketmq基本安装与使用(一)
- java oom dump_Java OOM 内存溢出分析
- 幼儿园计算机知识培训内容,幼儿园教师计算机培训计划
- vue 计算属性和data_Vue计算属性原理和使用场景
- python随机抽取人名_用Python打造一个CRM系统(五)
- ps cs6 磨皮插件_PS后期磨皮插件美颜润肤如此简单,效果比DR3要好
- java怎么获取固定的日期,如何获取一个指定时间的java.util.Date对象
- 人月神话贯彻执行_上古神话知识梳理,精华帖
- 使用网站模板快速建站_建站工具使用教程看了就懂网站建设
- 自学计算机二级office用什么书,暑假里想要自学计算机二级office有哪些什么好的建议...