堆排序重建堆的时间复杂度_堆排序

本文介绍一种神奇的排序方法——堆排序。

堆排序不像插入排序和归并排序那样直观,它利用了一种称为堆的数据结构。

堆本质上是一个数组,但我们将其当做一个近似的完全二叉树来看待。树上的每一个结点对应数组中的一个元素,按层排列。除了最底层外,该树是完全充满的,而且是从左向右填充。

下图(a)为我们想象中堆的结构,而(b)则是其实际存储形式。

b4bc8cb8658dba691068a592d339d194.png

最大堆的特点是,每一个结点都比它的两个孩子结点大(如果孩子结点存在的话)。类似地,最小堆的每一个结点都比它的两个孩子结点小。但需要注意的是,大小关系只存在于根节点与其孩子结点中,兄弟结点或不同子树中的结点没有大小关系。根据这条性质,很容易发现,最大堆的根结点是所有元素的最大值,最小堆的根结点是所有元素的最小值。

无论是最大堆还是最小堆,它们的工作原理都是一样的,因此本文以最大堆为例,因为使用最大堆排序得到的恰好是递增序列。

堆具有两个重要操作——建堆和维护堆。

维护堆

先来介绍维护堆,这是堆最基本的操作。当一个根节点的左子树和右子树都是最大堆,而根结点对应的树却不是最大堆时,说明根结点小于其左孩子或右孩子。此时,我们需要令根结点“逐级下降”,与其某个孩子结点交换位置,使整个树重新满足最大堆的性质。

维护堆的过程如下图所示。

d82d92bed2542ea73ba9c8e35c6c6583.png

图中,我们要维护结点2对应的堆。由图(a)可以看到,其左子树符合最大堆的性质,其右子树也符合最大堆的性质,但其本身却小于左孩子和右孩子。此时,我们要从结点2、结点4和结点5中选择最大的结点,将其与结点2交换位置,从而得到图(b)所示的状态。接下来,结点4又不满足最大堆的性质了,再将其与结点8和结点9比较大小,从而交换结点4与结点9,得到图(c)的状态。此时,结点9已经成为叶子结点,维护堆的操作结束。

建堆

建堆将一个无序的数组建立成满足堆性质的数组,建堆的过程如下图所示。

3479261eb6c81c9e7e754855be260d48.png

首先,给定无序数组A。先将其按从上到下、从左到右的顺序建立二叉树,如图(a)所示。然后从后向前找到第一个不是叶子结点的结点,对该结点执行维护堆操作,完成后该结点对应的子树就满足了堆的性质。继续向前遍历所有的结点,重复维护堆操作,直到根结点对应的堆(即完整的堆)满足堆的性质。

堆排序

现在我们可以考虑如何使用堆实现一个排序算法。由于最大堆的根结点保存了数组的最大值,因此可以每次将根结点的值从数组中取出,再令剩下的元素重新形成堆,如此往复,就可以依次从大到小取出数组中的所有元素。

下图所示为堆排序的完整流程。

09593a735f011f7e46617f04319da4ac.png

图(a)为建堆后的结果。从(a)到(b)的具体过程为,取出根结点16放到末尾,然后把最后一个叶子结点1放到根结点的位置(注意此时堆的元素少了一个),执行一次“维护堆”操作,结果就成了图(b)所示的样子。不断重复这个过程,直到所有结点都离开了堆,堆排序算法就结束了。结果是一个从小到大的数组。

堆排序是原址排序,不需要额外的内存空间,因为堆中元素只存在交换位置的操作,数组在原有地址里排序。

性能分析

堆排序应用了一次建堆操作和

次维护堆操作。

维护堆的时间复杂度很容易分析,对于元素个数为

的堆,其高度为
,那么任意一个结点的维护堆操作时间复杂度应该是
,因为“逐级下降”的级数最多为树的高度,且一般小于树的高度。

建堆操作内部调用了

次维护堆,因此时间复杂度应该是
。但实际上,这并非紧确界,因为每个结点调用维护堆时它们各自的高度是不一样的,如果把各自的高度也考虑进去,就能得到更准确的结果。具体来说,对于高度为
的子树,其维护堆的代价为
。而高度为
的子树有多少个呢?答案是最多有
个,其中
是从树根到高度为
的这一层的距离。然后我们把
累加起来,即可得到总的时间复杂度。

其中,第三行做了一次放缩,将有限级数求和扩大到无限级数求和,而后者的极限为常数2。于是,我们得到建堆操作更紧确的渐进时间复杂度

最后,堆排序的时间复杂度为

实现代码

具体实现时,需要先实现堆数据结构,然后利用该数据结构实现堆排序。实现原理本文已经解释清楚,因此不再赘述,代码位于Heap.java和HeapSort.java。

堆的应用

堆除了用于排序之外,还有其它众多用途。

最常见的用途莫过于优先队列。将普通队列建堆,即可得到一个优先队列,最大堆对应着最大优先队列,最小堆对应着最小优先队列。当用户需要从优先队列中取出一个元素时,直接返回堆顶元素,然后将堆的末尾元素补充到堆顶,并执行一次维护堆操作。当用户需要向优先队列插入数据时,直接将该元素置于堆的末尾,然后做一次“逐级上升”的维护堆操作。这样保证了在插入或取出数据时堆的性质都得以保留。

优先队列具有很多实际用途,比如从海量数据中搜索top k、作业调度器、合并有序序列、查找中位数等等,这里就不一一介绍了,感兴趣的同学可以自行查找相关资料。

参考资料

堆的应用 MakeSail

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值