热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

优点队列(堆)

一、模型优先队列是允许至少下列两种操作的数据结构:insert(插入),以及deleteMin(删除最小者&

一、模型

优先队列是允许至少下列两种操作的数据结构:insert(插入),以及deleteMin(删除最小者),函数的作用是找出、返回并删除优点队列中最小的元素。insert操作等价于enqueue(入队),而deleteMin则是队列运算dequeue(出队)在优点队列中的等价操作。


二、简单实现

有几种明显的方法实现优先队列。我们可以使用一个简单链表在表头以O(1)执行插入操作,并且遍历该链表以删除最小元,但是这需要O(N)时间。另一种方法是,始种让链表保持排序状态,这使得插入代价高昂O(N),而deleteMin花费低廉O(1)。基于deleteMin的操作从不多于插入操作的事实,前者是更好的想法。

另一种实现优点队列的方法是使用二叉查找树,它对这两种操作的平均运行时间都是O(logN)。尽管插入是随机的,而删除则不是,但这个结论还是成立的。但是由于我们删除的唯一元素是最小元。反复初去左子树中的节点将会损害树的平衡,使得右子树加重。然而,右子树是随机的。在最坏的情形,即deleteMin将左子树删空的情形下,右子树拥有的元素最多也就是它应具有的元素数的两倍。这只是在它的期望深度上加了一个小常数。注意,通过使用平衡树,可以把这个界变成最坏情形的界,这将防止出现坏的插入序列。

使用查找树可能有些过头,因为它支持许许多多并不需要的操作。


三、二叉堆

二叉堆以最坏时间O(logN)支持insert和deleteMin两种操作。插入操作实际上将平均花费常数时间,若无删除操作的干扰,该结构的实现将以线性时间建立一个具有N项的优先队列。

类似二叉查找树,堆也有两个性质,即结构性和堆序性。恰似AVL树,对堆的一次操作可能破坏这两个性质中的一个,因此,堆的操作必须到堆的所有性质都被满足时才能终止。


3.1 结构性质

堆是一颗被完全填满的二叉树,有可能的例外是在底层,底层上的元素从左到右填入。这样的树称为完全二叉树。
下图是一个例子:
在这里插入图片描述
容易证明,一颗高为h的完全二叉树有2h2^h2h2h+1−12^{h+1}-12h+11。同时一个重要的观察发现,因为完全二叉树这么有规律,所以它可以用一个数组表示而不需要使用链。
下图中的数组对应上图中的堆:
在这里插入图片描述
对于数组中任一位置i上的元素,其左儿子在2i上,右儿子在左儿子后的单元(2i+1)中,它的父亲则在位置i/2上。因此,这里不仅不需要链,而且遍历该树所需要的操作也极为简单,在大部分计算机上运行很可能都非常快。这种实现的唯一问题在于,最大的堆大小需要事先估计,但一般这并不成问题(而且如果需要,可以重新调整大小)。

因此,一个堆结构可以有一个(comparable对象的)数组和一个代表当前堆的大小的整数组成。


3.2 堆序性质

使操作被快速执行的性质是堆序性质。由于我们想要能够快速地找出最小元,因此最小元应该在根上。如果考虑任意子树也应该是一个堆,那么任意节点就应该小于它的所有后裔。

应用这个逻辑,我们得到堆序性质。在一个堆中,对于每一个节点X,X的父亲中的关键字小于(或等于)X中的关键字,根节点除外(它没有父亲)。

下图中左边的树是一个堆,但是右面的树不是:
在这里插入图片描述
根据堆序性质,最小元总可以在根处找到。因此,我们以常数时间得到附加操作findMin


3.3 基本的堆操作


3.3.1 insert(插入)

为将一个元素X插入到堆中,我们在下一个可用位置创建一个空穴(hole),因为如果不创建空穴的话堆将不是完全树。如果X可以放在该空穴而并不破坏堆的序,那么插入完成。否则,我们把空穴的父节点上的元素移入该空穴中,这样,空穴就朝着根的方向上冒一步。继续该过程直到X能被放入空穴中为止。

下图表示为了插入14,我们在堆的下一个可用位置建立一个空穴。由于将14插入空穴破坏了堆序性质,因此将31移入该空穴。
在这里插入图片描述
继续这个过程,直到找到14的正确位置:
在这里插入图片描述

这种一般的策略叫做上滤。新元素在堆中上滤直到找出正确的位置。

其实我们可以在insert例程中通过反复实施操作直至建立正确的序来实现上滤过程,可是一次交换需要3条赋值语句。如果一个元素上滤d层,那么由于交换而实施的赋值操作就达到3d,而我们这里的方法只用到d+1次赋值。

如果要插入的元素是新的最小值,那么它将一直被推向顶端。这样一来会在某一时刻hol==1,程序跳出循环。如果欲插入元素是新的最小元而一直上滤到根处,那么这种插入的时间将长达O(logN)。平均来看,上滤终止的要早。现在已经证明,执行一次插入平均需要2.607次比较,因此平均insert将元素上移1.607层。


3.3.2 deleteMin(删除最小元)

deleteMin以类似插入的方式处理。找出最小元是容易的,困难的是删除它。当删除一个最小元时,要在根节点建立一个空穴。由于现在的堆少了一个元素,因此堆中最后一个元素X必须移动到该堆的某个地方。如果X可以被放到空穴中,那么deleteMin完成,不过这一般不太可能,因此我们将空穴的两个儿子中较小者移入空穴,这样就把空穴向下推了一层。重复该步骤直到X可以被放入空穴中。因此,我们的做法是将X置入沿着从根开始包括最小儿子的一条路径上的一个正确的位置。

下图中左边的图现实deleteMin之前的堆。删除13后,我们必须试图正确地将31放到堆中。31不能放在空穴中,因为这将破坏堆序性质。于是,我们把较小的儿子14置入空穴,同时空穴向下滑一层。重复该过程,由于31大于19,因此把19置入空穴,在更下一层上建立一个新的空穴。然后,因为31还是太大,于是再把26置入空穴,在底层又建立一个新的空穴。最后,我们得以将31置入空穴中。这种一般的策略叫做下滤。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这种操作最坏情形运行时间为O(logN)。平均而言,被放到根处的元素几乎下滤到堆的底层,因此平均运行时间为O(logN)。


四、优先队列的应用


4.1 选择问题

输入N个元素以及一个整数k,这N个元素可以是排序过的。我们需要找出第k个最大的元素。


4.1.1 算法A

为了简单起见,假设只考虑找出第k个最小的元素。该算法很简单:将N个元素读入一个数组。然后对该数组应用buildHeap算法。最后,执行k次deleteMin操作。从该堆最后提取的元素就是我们的答案。

使用buildHeap构造堆的最坏情形用时是O(N),而每次deleteMin用时O(logN)。由于有k次deleteMin,因此得到总的运行时间为O(N+klogN)。


4.1.2 算法B

在任一时刻我们都将保留k个最大元素的集合S。在前k个元素读入以后,当再读入一个新的元素时,该元素将与第k个最大元素进行比较,记这个第k个最大的元素为SkS_kSk。注意,SkS_kSk是S中最小的元素。如果新的元素更大,那么用新元素代替S中的SkS_kSk。此时,S将有一个新的最小元素,它可能是新添加进的元素,也可能不是。在输入完成时,我们找到了S中最小的元素,将其返回,就是我们需要的答案。

这里使用一个堆来实现S。前k个元素通过调用一次buildHeap以总时间O(k)被置入堆中。处理每个其余的元素的时间为O(1),用于检测是否元素进入S,再加入时间O(logk),用于在必要时删除SkS_kSk并插入新元素。因此,总的时间是O(k+(N-k)logk)=O(Nlogk)。该算法也给出找出中位数的时间界O(NlogN)。


推荐阅读
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文介绍了如何通过维持两个堆来获取一个数据流中的中位数。通过使用最大堆和最小堆,分别保存数据流中较小的一半和较大的一半数值,可以保证两个堆的大小差距为1或0。如果数据流中的数量为奇数,则中位数为较大堆的最大值;如果数量为偶数,则中位数为较大堆的最大值和较小堆的最小值的平均值。可以使用优先队列来实现堆的功能。本文还提供了相应的Java代码实现。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文介绍了栈和队列的区别及其特点。栈是一种先进后出的线性表,只能在表的一端进行插入和删除操作;队列是一种先进先出的线性表,只能在表的一端进行插入和在另一端进行删除操作。栈和队列是两种广泛使用的线性数据结构,它们的基本操作具有特殊性。栈的遍历需要遍历整个栈才能取出数据,并需要为数据开辟临时空间,而队列基于地址指针进行遍历,可以从头或尾部开始遍历,但不能同时遍历,且无需开辟临时空间。栈和队列在程序设计中具有重要应用。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 开发笔记:快速排序和堆排序
    本文由编程笔记#小编为大家整理,主要介绍了快速排序和堆排序相关的知识,希望对你有一定的参考价值。快速排序思想:在partition中,首先以最右边的值作为划分值x,分别维护小于 ... [详细]
  • 尾部|柜台_Java并发线程池篇附场景分析
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客 ... [详细]
  • 十大经典排序算法动图演示+Python实现
    本文介绍了十大经典排序算法的原理、演示和Python实现。排序算法分为内部排序和外部排序,常见的内部排序算法有插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。文章还解释了时间复杂度和稳定性的概念,并提供了相关的名词解释。 ... [详细]
  • java线程池的实现原理源码分析
    这篇文章主要介绍“java线程池的实现原理源码分析”,在日常操作中,相信很多人在java线程池的实现原理源码分析问题上存在疑惑,小编查阅了各式资 ... [详细]
  • RingBuffer,或者说CircularBuffer,是一个长度固定的缓冲区,当从一端插入元素超过指定的最大长度时,缓冲区另一端的元素 ... [详细]
  • 生产环境下JVM调优参数的设置实例
     正文前先来一波福利推荐: 福利一:百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。福利二 ... [详细]
author-avatar
没有丝袜姑娘
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有