Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

堆排序

复习

  • 堆:父不大于子,堆顶是最值
  • 优先队列:每次取出优先级最高的元素
  • 数组:堆可以用数组紧凑表示

TL;DR

  • 堆排序先把数组建成堆,再反复取出堆顶
  • 每取出一个,就把剩下的部分重新调整成堆
  • 时间稳定在 O(n log n),并且就地排序
  • 它是基于比较的排序,通常不稳定

正文

  我们学过堆,也学过用堆实现优先队列。把这两样东西用到排序上,就得到了堆排序(heapsort)。

反复取堆顶

  堆的特点是:堆顶永远是最值。 堆排序就抓住这一点:

  1. 先把整个数组整理成一个(比如最大堆,堆顶是最大值)
  2. 把堆顶(最大值)和末尾元素交换,这样最大值就放到了它最终的位置
  3. 把堆的有效范围缩小一格,再对新的堆顶做“下沉”,恢复堆的性质
  4. 重复,直到堆里只剩一个元素

  每次取出一个最大值放到末尾,从后往前,数组就逐渐有序了。整个过程就像是用优先队列不断地“弹出最值”,只不过是在同一个数组里就地完成。

稳定的 O(n log n)

  建堆的过程可以做到 O(n);之后要取出 n 个元素,每次取出后调整堆是 O(log n),合计 O(n log n)

  更重要的是:这个代价是稳定的,不像快排会因基准不佳而退化。无论输入怎样,堆排序都保持 O(n log n)

代价与缺点

  堆排序的优点是就地——不需要像归并那样额外开一个数组,也不像快排有最坏退化。

  代价则在于:它虽然就地,却不像快排那样对缓存友好——访问的元素在数组里跳来跳去。而且它是不稳定的,相等的元素在反复交换中可能被打乱顺序。

  于是,三种 O(n log n) 排序各有性格:

  • 归并:稳定、最坏也稳定,但要额外空间
  • 快排:就地、平均最快,但最坏会退化、不稳定
  • 堆排:就地、最坏稳定,但常数偏大、不稳定

  没有全能的选手,只有各适其用的工具。 这也正是这几章反复想传达的观念。

思考题 1

  堆排序是怎样利用堆“堆顶是最值”这一性质的?

思考题 2

  堆排序可以就地完成,它的代价是什么?

小结

知识点

  • 堆排序先建堆,再反复取出堆顶
  • 每轮把堆顶与末尾交换,再缩小堆并调整
  • 时间稳定为 O(n log n),就地完成
  • 常数较大且不稳定

参考资料

  1. Wikipedia(zh):堆排序:利用堆进行排序的算法
  2. Wikipedia(zh):二叉堆:堆排序依赖的结构

思考题答案(仅供参考)

思考题 1

  因为堆顶始终是当前堆里的最大(或最小)值。堆排序反复取出堆顶,就相当于不断取出当前最大值,把它放到最终位置,再调整剩余部分,从而完成排序。

思考题 2

  代价主要是:堆内元素的访问位置跳来跳去,对缓存不够友好,常数偏大;而且它是基于比较、通常不稳定,相等元素的相对顺序可能被打乱。就地省下了空间,却付出了这些代价。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪