笔试算法题(57):基于堆的优先级队列实现和性能分析(Priority Queue based on Heap)
议题:基于堆的优先级队列(最大堆实现)
分析:
堆有序(Heap-Ordered):每个节点的键值大于等于该节点的所有孩子节点中的键值(如果有的话),而堆数据结构的所有节点都按照完全有序二叉树 排。当使用数组存储这种数据结构时,在数组大小限制和堆大小限制下,如果当前节点下标为i,其父亲节点下标为i/2,左右孩子结点下标分别为 2i,2i+1(如果计算值没有超出队列大小范围);
使用堆有序完全二叉树(Complete Binary Tree)表示优先队列,所有操作即使最坏情况下的运行时间也只是对数时间,因为任何操作(除合并)表现在完全二叉树中只是从某一层的节点到另一层节点的路径,而这种路径永远不会使得运行时间超过㏒N
堆有序完全二叉树表示有如下性质(在一棵有N个节点的完全树种):
所有从根节点到叶节点的路径上大约有㏒N个节点;
大约有N/2的节点位于树的底部(叶子节点);
大约有N/4个带孩子的节点位于次最底层;
大约有N/8个带孙子的节点位于次次最底层;
每一代的节点数大约为下一代节点数的一半,也就是说完全树最多有㏒N层;
针对优先队列的算法都是首先进行一些修改,然后遍历堆以确保所有的节点都满足优先队列的性质,这个过程称为被堆化(Heapifying)。而具体的修改有两种情况:
提升一些节点的优先级(或者在堆的底部添加新的节点),需要向上遍历堆以恢复堆的限制性质;
降低一些节点的优先级(或者使用一个新的节点替代根节点处的节点),需要向下遍历堆以恢复堆的限制性质;
fixup和fixdown操作:首先需要两个函数维护队列满足堆的基本性质,fixup和fixdown维护堆的性质(数组下标从1开始,0作为某些标志);
insert操作:实际上是在队列末尾添加新元素项,队列大小增加1,并使用fixup进行恢复;
getMaximum操作:获取最大项操作实际上是返回根节点元素,然后将队列末尾的项放置到根节点,队列大小减1,并使用fixDown进行恢复;
MyMaximumHeap是类构造函数:利用其结合insert操作,也就是自顶向下的构建策略,向一个空堆里面不断插入新元素 (insert,fixUp),并增加堆大小,最坏构建时间为O(NlogN),平均构建时间为O(N);这个方法运行时间在最坏情况下与N㏒N成正比 (当每个新元素都是当前堆中的最大项,恢复时需要向上遍历到根节点,所以N个元素进行N次的fixup回复,每次fixup移动㏒N步),但平均时间为线 性(如果元素随机出现,则每一项平均只向上移动少数的几层);
constructMaximumHeap操作:是将给定的数组a构建成一个最大堆;也就是自底向上的构建策略,从原始数组的位置length/2处开始 向位置1循环向下处理每一个元素(fixDown),最坏构建时间为O(NlogN),平均构建时间为O(N);使用自底向上的构建方法,从最后一个拥有 子节点的子树节点开始,由于在随机情况下先前进行的的fixDown操作会为后来执行的的fixDown操作提供排序信息(不断将指定子树中的最大值向上 移动),所以运行时间为线性;
resetPriority操作:是改变array中指定位置k的元素的优先级,如果新的优先级较原来的大,则向上移动;否则向下移动;时间复杂度为O(lgN);
priorityQueueSort操作:优先队列首先在给定的数组中构建最大堆,然后将堆最末尾的元素和根节点进行交换,将堆大小减一,使用 fixdown进行恢复,重复上述过程。下向(sortdown)排序过程和选择排序类似,都是在剩余的序列中查找最大的元素,不同的是下向排序使用一中 更加有效的方式处理剩余元素,从而进行最大元素的选择;
maximumKthSelect操作:堆排序同样可以用于求N个项中的第K个最大项的选择问题,当从堆中取出K-1个元素之后,当前堆顶处就是第K个最 大项。对于从N个元素中选择最大Kth元素的问题,有两种堆实现策略:一种是利用所有N个元素构建最大堆,并k-1此删除堆顶元素,之后array[1] 的元素就是最大Kth元素,比较次数为2N+2klgN;一种是利用原始数据的前k个元素构建一个最小堆,然后将剩余的N-k个元素依次插入此最小堆,如 果新元素小于当前堆顶元素则删除当前堆顶元素,并恢复最大堆的性质,所有元素都插入之后得到的堆顶元素就是最大Kth元素,比较次数为2k+2(N- k)lgk;第二种方法的优势在于:首先当N非常大的时候,构建k大小的堆只有较少的空间消耗;然后当k远小于N的时候,lgk具有O(1)的时间上限;
样例:
- struct MyMaximumHeap {
- /**
- * 堆数据从索引1开始存储
- * 1表示堆顶元素的位置
- * capacity表示堆的最大容量
- * count表示最后一个有效堆元素的位置
- * */
- int *array;
- int capacity;
- int count;
- /**
- * 此构造函数可以作为:
- * 无参数构造函数
- * 指定cap参数构造函数
- * 构建一个空的最大堆(没有任何初始项),需要通过调用
- * insert方法插入具体的堆数据;如果按照此方法构建大小为N
- * 的最大堆,最坏时间为O(NlogN),但是平均时间为O(N)
- * */
- MyMaximumHeap(int cap=):
- capacity(cap), count(), array((int*)malloc(cap*sizeof(int))) {}
- /**
- * 此方法将指定位置k的元素向堆顶移动
- * 由于仅需要当前节点与其父亲节点的比较,所以从
- * 的比较次数为lgN
- * */
- void fixUp(int *array, int k) {
- int temp;
- /**
- * 首先检查当前节点是否为根节点,
- * 然后检查当前节点k是否比其父亲节点k/2大
- * 接着如果满足条件则进行交换
- * 最后循环处理父亲节点
- * */
- while(k> && array[k/] < array[k]) {
- temp=array[k];
- array[k]=array[k/];
- array[k/]=temp;
- k=k/;
- }
- }
- /**
- * 此方法将制定位置k的元素向堆底部移动
- * 由于需要从父亲,左儿子和右儿子之间选出一个最大值,所以需要两次比较
- * 所以总计2*lgN次比较
- * */
- void fixDown(int *array, int k, int count) {
- int temp, max;
- /**
- * 外循环保证以当前节点k至少有左儿子节点
- * 然后寻找父亲节点,和左右儿子节点中的最大
- * 节点,如果父节点k最大,则跳出循环
- * */
- while(*k <=count) {
- max=k;
- if(array[*k] > array[max])
- max=*k;
- /**
- * 注意访问右儿子之前需要确定其存在
- * */
- if(*k+<=count && array[*k+]>array[max])
- max=*k+;
- /**
- * 如果父亲节点是最大的节点,则方法结束
- * */
- if(max==k) break;
- temp=array[max];
- array[max]=array[k];
- array[k]=temp;
- /**
- * 循环处理交换之后的儿子节点
- * */
- k=max;
- }
- }
- bool isEmpty() {
- return count==;
- }
- bool isFull() {
- return count==capacity;
- }
- /**
- * 插入操作是将新元素插到array的末尾,
- * 然后调用fixUp操作进行最大堆化
- * 此方法的比较次数小于lgN
- * */
- bool insert(int n) {
- if(isFull())
- return false;
- array[++count]=n;
- fixUp(array, count);
- return true;
- }
- /**
- * 获取最大项操作是将array末尾的元素
- * 复制到array[1]处,然后调用fixDown
- * 操作进行最大堆化,然后返回原始的堆顶
- * 元素;
- * 此方法的比较次数小于2*lgN
- * */
- bool getMaximum(int *n) {
- if(isEmpty())
- return false;
- int temp=array[];
- array[]=array[count--];
- fixDown(array,, count);
- *n=temp;
- return true;
- }
- /**
- * 此方法将指定位置k上的元素的优先级修改成n
- * 注意如果k的范围超出有效堆元素范围,则直接返回
- * 如果n的值与array[k]相等,则直接返回
- * */
- void resetPriority(int k, int n) {
- if(k<= || k>=count)
- return;
- if(n==array[k])
- return;
- array[k]=n;
- if(n>array[k]) {
- fixUp(array, k);
- } else if(n<array[k]) {
- fixDown(array, k, count);
- }
- }
- /**
- * 此方法将给定的array构建成最大堆,与MyMaximumHeap
- * 类没有任何关系,仅是一个为其他数组提供的工具方法
- * */
- void constructMaximumHeap(int *a, int length) {
- /**
- * index初始化为堆中最后一个拥有孩子节点的内部节点
- * */
- int index=length/;
- while(index>=) {
- /**
- * 对以index索引的根节点的子树调用fixDown
- * */
- fixDown(a, index, length);
- index--;
- }
- }
- /**
- * 堆排序使用少于2*NlgN此比较实现N个元素的排序
- * */
- void priorityQueueSort(int *a, int length) {
- /**
- * 首先利用自底向上的策略将数组a最大堆化
- * */
- constructMaximumHeap(a, length);
- int temp;
- /**
- * 然后依次将堆顶元素array[1]取出,由于取出之后
- * 堆大小减小,所以实现数组的末尾会空出位置用于
- * 存放当前取出的最大值;
- * 在调用fixDown之前需要检查length的长度是否已经
- * 小于2;
- * */
- while(true) {
- temp=a[length];
- a[length]=a[];
- a[]=temp;
- length--;
- if(length<=) break;
- fixDown(a, , length);
- }
- }
- int maximumKthSelect(int *a, int length, int k) {
- constructMaximumHeap(a, length);
- int temp;
- /**
- * 如果打算取第K大的元素,则取出最大的K-1个最大元素之后
- * 当前堆顶的元素array[1]就是滴K大的元素
- * 如果不考虑数组a的完整性,则取出的最大K-1个元素可以
- * 不同复制到数组末尾,而直接丢弃;
- * */
- for(int i=;i<k-;i++) {
- temp=a[length];
- a[length]=a[];
- a[]=temp;
- length--;
- if(length<=) {
- printf("\ncurrent heap's elements are less than k\n");
- return -;
- }
- fixDown(a, , length);
- }
- return array[];
- }
- };
- int main() {
- MyMaximumHeap *pq=new MyMaximumHeap();
- int temp;
- printf("\nplease input integers:\n");
- printf("\n1. you can at most input 10 integers\n");
- printf("\n2. you can input -1 to finish\n");
- scanf("%d",&temp);
- while(pq->insert(temp)) {
- scanf("%d",&temp);
- if(temp==-) break;
- }
- if(temp!=-) {
- printf("\nthe last integer (%d) is dismissed for not enough room\n", temp);
- }
- printf("\nget the maximual integer in order\n");
- while(pq->getMaximum(&temp)) {
- printf("%d, ",temp);
- }
- return ;
- }
补充:
由于在大小为N的堆中没有一条路径包含的元素会多于㏒N,所以当作用于一个N个元素的序列时,插入运算需要的比较次数不会超过㏒N次(仅与其父节点比较),每个节点进行一次比较判断是否比父节点大;
获取最大项操作需要的比较次数不会超过2㏒N(左右子节点谁大,父子节点谁大),每个节点需要进行两次比较:一次选出较大的孩子结点,另一次决定父节点是否与孩子结点交换;
另外当改变队列中某一个节点的优先级时,同样可以使用fixup和fixdown实现,提升优先级使用fixup,降低优先级时使用fixdown;
自底向上构建最大堆:首先使用一个循环从数组的中间到左端开始构建堆,然后不断缩减堆的大小并结合fixDown恢复堆性质从而保证每一次取出的都是剩下的堆中最大的元素。自底向上的构建过程花费线性时间,第二个循环中的堆排序使用少于2N㏒N次比较来排序N个元素;
堆的优先队列实现中,不存在让堆排序运行时间大大上升的输入序列(不同于快速排序),不存在需要使用与待排序序列N成比例的额外空间(不同于归并排序);
对于堆排序算法而言,优势:首先使用一个循环从数组的中间到左端开始构建堆,然后不断缩减堆的大小并结合fixDown恢复堆性质从而保证每一次取出的都 是剩下的堆中最大的元素。性质:不存在让堆排序运行时间大大上升(效率低下)的输入序列(不同于快速排序),不存在需要使用与待排序序列N成比例的额外空 间(不同于归并排序)时间:自底向上的构建过程花费线性时间,第二个循环中(while排序循环)的堆排序使用少于2N㏒N次比较来排序N个元素。使用堆 排序求第K最大项时,当k与N接近时,时间开销为线性,否则为K㏒N;
在已经实现了的优先级队列实现中没有哪一种实现能同时有效的解决删除最大项,插入和合并这三种操作(这里的有效实现指的是使用㏒N时间实现):
无序链表: 可快速插入(常数)和合并(常数), 但删除最大项慢(线性);
有序链表: 可快速删除最大项(常数), 但插入和合并慢(线性);
堆实现: 可快速插入(㏒N)和删除最大项(常数), 但合并慢(N㏒N):
堆排序算法的总结:
堆排序算法为非稳定排序;
可以用于解决求序列中第K个最大项(最小项)的选择问题,也就是在第K次删除最大项的时候停止即可;
一开始在构建堆的时候,从最后一个拥有子节点的内部节点开始构建,这样可以保证自底向上的每三个相邻节点(父节点,左子节点,右子节点)遵守堆的性质,到 了堆上部的时候就能保证靠近根节点的节点是相对较大的节点。在之后删除根节点,并将堆最后一个节点(较小节点)移到根节点处,然后进行fixDown,这 样做的目的是在根节点的左右两个节点中又选出最大的节点放到根节点处,但是几乎每一次的fixDown都会使得移动节点几乎到达堆的底部,这对 fixDown来讲是明显的性能损耗;
如果需要改进堆排序算法,可以考虑使用三叉树或者更多的完全分支,这样会降低树的高度,从而使得对数的底从2变成3或者更高,从而可以提升运行性能;
堆排序与归并排序之间的选择实际上简化为非稳定排序和需要使用额外内存方法之间的选择;
堆排序与快速排序之间的选择实际上简化为最坏情况最好和平均性能最好之间的选择;
笔试算法题(57):基于堆的优先级队列实现和性能分析(Priority Queue based on Heap)的更多相关文章
- 前端如何应对笔试算法题?(用node编程)
用nodeJs写算法题 咱们前端使用算法的地方不多,但是为了校招笔试,不得不针对算法题去练习呀! 好不容易下定决心 攻克算法题.发现js并不能像c语言一样自建输入输出流.只能回去学习c语言了吗?其实不 ...
- 【数据结构与算法Python版学习笔记】树——利用二叉堆实现优先级队列
概念 队列有一个重要的变体,叫作优先级队列. 和队列一样,优先级队列从头部移除元素,不过元素的逻辑顺序是由优先级决定的. 优先级最高的元素在最前,优先级最低的元素在最后. 实现优先级队列的经典方法是使 ...
- 笔试算法题(58):二分查找树性能分析(Binary Search Tree Performance Analysis)
议题:二分查找树性能分析(Binary Search Tree Performance Analysis) 分析: 二叉搜索树(Binary Search Tree,BST)是一颗典型的二叉树,同时任 ...
- 笔试算法题(45):简介 - AC自动机(Aho-Corasick Automation)
议题:AC自动机(Aho-Corasick Automation) 分析: 此算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一:一个常见的例子就是给定N个单词,给定包含M个字符的文章,要求 ...
- Java笔记(十)堆与优先级队列
优先级队列 一.PriorityQueue PriorityQueue是优先级队列,它实现了Queue接口,它的队列长度 没有限制,与一般队列的区别是,它有优先级概念,每个元素都有优先 级,队头的元素 ...
- Java集合总结(三):堆与优先级队列
堆 满二叉树:满二叉树是指,除了最后一层外,每个节点都有两个孩子,而最后一层都是叶子节点,都没有孩子. 完全二叉树:完全二叉树不要求最后一层是满的,但如果不满,则要求所有节点必须集中在最左边,从左到右 ...
- 如何基于RabbitMQ实现优先级队列
概述 由于种种原因,RabbitMQ到目前为止,官方还没有实现优先级队列,只实现了Consumer的优先级处理. 但是,迫于种种原因,应用层面上又需要优先级队列,因此需求来了:如何为RabbitMQ加 ...
- 笔试算法题(46):简介 - 二叉堆 & 二项树 & 二项堆 & 斐波那契堆
二叉堆(Binary Heap) 二叉堆是完全二叉树(或者近似完全二叉树):其满足堆的特性:父节点的值>=(<=)任何一个子节点的键值,并且每个左子树或者右子树都是一 个二叉堆(最小堆或者 ...
- php笔试算法题:顺时针打印矩阵坐标-蛇形算法
这几天参加面试,本来笔试比较简单,但是在面试的时候,技术面试官说让我现场写一个算法,顺时针打印矩阵的坐标,如图所示 顺序为,0,1,2,3,4,9,14,19,24,23,22,21,20,15,10 ...
随机推荐
- Spring--quartz中cronExpression配置说明
Spring--quartz中cronExpression Java代码 字段 允许值 允许的特殊字符 秒 0-59 , - * / 分 ...
- ubuntu16.04下使用python3开发时,安装pip3与scrapy,升级pip3
1)安装pip3: sudo apt-get install python3-pip 2)安装scrapy sudo pip3 install scrapy 若出现版本过低问题: pip3 insta ...
- Xenocode Postbuild 2010 for .NET 使用
代码混淆工具 参考地址1:http://blog.csdn.net/yanpingsoft/article/details/7997212 参考地址2:http://www.cnblogs.com/w ...
- bzoj 2303: [Apio2011]方格染色【并查集】
画图可知,每一行的状态转移到下一行只有两种:奇数列不变,偶数列^1:偶数列不变,奇数列^1 所以同一行相邻的变革染色格子要放到同一个并查集里,表示这个联通块里的列是联动的 最后统计下联通块数(不包括第 ...
- Spark SQL概念学习系列之Spark SQL入门
前言 第1章 为什么Spark SQL? 第2章 Spark SQL运行架构 第3章 Spark SQL组件之解析 第4章 深入了解Spark SQL运行计划 第5章 测试环境之搭建 第6章 ...
- DecimalFormat数字格式化用法“0”和“#”的区别
1. 以“0”补位时: 如果数字少了,就会补“0”,小数和整数都会补: 如果数字多了,就切掉,但只切小数的末尾,整数不能切: 同时被切掉的小数位会进行四舍五入处理. 2. 以“#”补位时: 如果数字少 ...
- [ZPG TEST 114] 阿狸的英文名【水题】
1. 阿狸的英文名 阿狸最近想起一个英文名,于是他在网上查了很多个名字.他发现一些名字可以由两个不同的名字各取一部分得来,例如John(约翰)的前缀 “John”和Robinson(鲁滨逊) ...
- bootmanager is missing
问题描述: 在计算机管理->存储->磁盘管理中,因误操作,将D盘设置了"将分区标记为活动分区(M)",导致重启时无法无法进入系统,提示"bootmanager ...
- Oracle报错:“ORA-18008: 无法找到 OUTLN 方案 ”的解决方案
Oracle报错:“ORA-18008: 无法找到 OUTLN 方案 ”的解决方案 2.修改replication_dependency_tracking参数 SQL> alter syst ...
- 216 Combination Sum III 组合总和 III
找出所有可能的 k 个数,使其相加之和为 n,只允许使用数字1-9,并且每一种组合中的数字是唯一的.示例 1:输入: k = 3, n = 7输出:[[1,2,4]]示例 2:输入: k = 3, n ...