提到排序,大多数程序员脑子里第一个蹦出来的永远是快速排序。我工作这些年面过不少人,聊到堆排序的时候,能讲出“先建堆,再不断取根”的不少,但能把自底向上建堆为什么是 O(n)、换完根之后为什么要重新下沉、它为什么不稳定这三件事一次讲全的,几乎没遇到过。
堆排序本质上是一种树形排序:它不直接维护一个有序数组,而是先把数据组织成一棵藏在数组里的完全二叉树,让任意父节点都比子节点更大(或更小)。这种约束看起来松散,却足够保证根节点就是全局最大(或最小)。接下来每次把根节点捞出来放到末尾,再让剩余数据重新满足约束,数组就一点一点被排好了。堆排序算法也因此成为理解“优先队列”最自然的入口,更是 TopK、调度、图算法里那些“动态取最值”场景的底层思想。
这篇文章写给想真正吃透堆排序的开发者。不是背代码,而是把原理、手写实现、边界坑、复杂度推导和应用场景串成一条线。无论你是准备面试,还是要在工程里解决海量数据取前 K,都应该完完整整走一遍。
1. 堆排序的真正价值:被快排压着打,但树形思想无处不在
在很多人印象里,堆排序是一个“理论很强、实战一般”的算法。最坏时间复杂度 O(n log n),最好也是 O(n log n),平均还是 O(n log n),空间 O(1),表面上看几乎完美。但现实是,各语言默认排序几乎都不用它:Python 的 Timsort、C++ 的 std::sort 内部是 introsort,Java 的 Arrays.sort 对对象用的是 Timsort。为什么堆排序这么“标准”,却很少成为默认排序?
核心原因有两点。第一,快速排序的变体在绝大多数真实数据上更快,因为它对数组的访问是连续的,对 CPU 缓存非常友好;堆排序每次下沉都要在数组里做跳跃式访问,父子节点之间相隔很远,缓存命中率低很多。第二,堆排序的常数项偏大,即便复杂度同为 O(n log n),实际跑起来往往比快排慢。工程界追求的是真实数据上的性能,不是理论最坏情况下的保证。
但为什么堆排序依然值得你彻底学透?因为它有两个快排给不了的东西。
第一个是“最坏情况下有保证”。快速排序如果选主元选得差,会退化到 O(n²),这是很多线上事故的根源;堆排序无论输入是正序、逆序还是随机,始终是 O(n log n)。这在社会中代表“可控性”,有些低延迟系统宁可损失一点平均性能,
