堆排序
看完你能手推建堆与每次下沉的全部步骤,并说清为何是就地排序
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
堆排序
看完你能手推建堆与每次下沉的全部步骤,并说清为何是就地排序
回顾:完全二叉堆
上一页说堆排序要靠「堆」这个结构。你可能已经见过优先级队列:每次取出的总是最优先的那个。堆正是它的底层实现——关键在两条规则:谁在上、怎么摆。
层级里的上级权限对应节点值更大,但同层不做比较
算法总体思路
上一页我们建好了一个大顶堆:堆顶永远是最大值。现在换个角度想——每一轮把最大值「选」出来放到末尾,这不就是选择排序吗?堆只是给了我们一个找最大值的高效结构。
冠军(堆顶)领奖后退场,剩下的选手重赛一轮定新冠军
两阶段:建堆 + 反复摘顶
从无序数组出发,先花一次代价把数组调成大顶堆,再反复交换堆顶与末位并缩堆,直到全部有序。
为什么用堆做选择
上一页我们知道堆排序就是反复摘顶。可为什么要先费力建堆?回忆选择排序:每轮线性扫描找最大值要 O(n),重复 n 轮就是 O(n²)。堆正是为了解决「找最值太慢」这个瓶颈。
冠军只需打赢 logn 场对手,对应堆顶下沉一层只比较一次
什么是就地排序
前面几页我们说了:堆排序=建堆+反复摘顶。你可能注意到,摘出来的元素往哪放了?答案是和堆尾交换——整个排序就在原数组里腾挪,几乎不借外部空间,这就是就地排序。
不倒到桌上另铺一层,直接在原抽屉内换位完成整理
堆区间与已排序区间
把数组横着看:左段仍是堆,右段是已排好的最大值,虚线边界随每轮交换左移一格。
交换—下滤—收缩循环
摘顶循环每轮三步:交换、下滤、收缩,缺一不可。
大顶堆还是小顶堆
上一页的循环把最大值换到数组尾部——「最大」从哪来?答案取决于堆型:升序要每次摘出最大,所以用大顶堆。如果误用小顶堆,摘出的是最小值,放到尾部就成了降序,边界必须辨析清楚。
先出的是最重的沉在底部(数组尾),轻的留在上层
自上而下 vs 自下而上建堆
上一页定了要用大顶堆,但堆不是天生的——拿到一个乱序数组,得先把它「堆化」。怎么建这个堆?有两条路线:一个个往里插,或者从下往上整批调整。代价差别大得惊人。
一个人一个人通知像逐个插入 O(nlogn),按层级群发像 Floyd 建堆 O(n)
Floyd 建堆:逐个下滤
Floyd 建堆从最后一个内部节点起逆序下滤,把零散子树合并成整体堆。
下滤 siftDown 实现
一段可运行的下滤代码:父亲不断与较大的孩子交换,直到恢复堆序。
四个关键点:没有孩子就停、选较大的孩子比较、父亲够大就提前终止、否则交换并继续下沉——每一处都决定下滤是否正确。
堆排序主体实现
Floyd 建堆 + 反复交换下滤,完整的就地堆排序落地。
L4-5 是 Floyd 建堆:从最后一个非叶节点倒序下滤;L9-11 摘顶循环:交换后堆区间收缩,end 同时是已排序区间的左边界。
复杂度与稳定性分析
前面已完整实现堆排序:Floyd 建堆加上反复摘顶。现在把账算清楚——为什么整体是 O(nlogn)?这个算法省下了什么,又付出了什么代价?
不借助新书架直接换书位置对应 O(1) 空间,但书被甩来甩去就乱了先后
实例:建堆阶段演示
以数组 [4, 6, 8, 5, 9] 为例,从最后一个非叶结点出发逐个下滤,把乱序数组建成大顶堆。
实例:反复摘顶阶段
每轮三连:交换、下滤、收缩,最大值逐个落到尾部。
自测:下一步是什么
给定堆快照,判断下一轮交换与下滤的结果
要点收束
- ✓两阶段缺一不可:Floyd 建堆 O(n),摘顶 O(n log n)
- ✓大顶堆配升序:摘顶后最大值沉到尾部
- ✓就地排序的本质:堆区间不断收缩,无需辅助数组
- ✓交换后必须下滤恢复堆序,再收缩边界
- ✓不稳定:长距离交换会打乱相等元素的相对次序
课后思考
三个问题先自己想五分钟,再看参考答案。
参考答案关键在依赖:下滤要求数据已是堆。从后往前时,每个节点的左右子树都已完成建堆,下滤才有意义;从前往后则子树还未处理,无法保证正确性。
参考答案摘出的最小值与堆尾交换,堆尾落在堆序的最深处,但它是当前最小值,方向就反了。若想就地放数组前端再整体翻转可行;否则需要额外数组,这也是大顶堆更自然的原因。
参考答案元素远大于内存或存在外存/磁盘时,堆排序的随机跳跃访问会频繁换页,局部性差;相比之下归并排序顺序访问更友好。这时倾向外部排序或多路归并。