相关笔记

概览

本节介绍 HEAPSORT 算法,它是堆数据结构最经典的应用之一。HEAPSORT 首先调用 BUILD-MAX-HEAP 将输入数组构建为最大堆,然后反复将堆顶的最大元素与堆的最后一个元素交换,缩小堆的规模,再通过 MAX-HEAPIFY 恢复堆性质。整个过程在原地完成,无需额外存储空间。

要点列表:

  • HEAPSORT 的运行时间为 ====,其中 BUILD-MAX-HEAP 耗时 次 MAX-HEAPIFY 调用共耗时
  • HEAPSORT 是原地排序算法(in-place),仅需常数级额外空间
  • HEAPSORT 是比较排序,其最坏情况运行时间 与下界匹配,因此是渐近最优的比较排序算法之一
  • 通过循环不变式可以严格证明排序的正确性

知识结构总览

flowchart TD
    A["6.4 堆排序算法 (HEAPSORT)"] --> B["算法流程"]
    A --> C["正确性证明"]
    A --> D["复杂度分析"]
    A --> E["算法特性"]

    B --> B1["第一步: BUILD-MAX-HEAP"]
    B --> B2["第二步: 循环 n-1 次"]
    B2 --> B2a["交换 A[1] 与 A[i]"]
    B2 --> B2b["heap-size 减 1"]
    B2 --> B2c["MAX-HEAPIFY(A, 1)"]

    C --> C1["循环不变式"]
    C1 --> C1a["初始化"]
    C1 --> C1b["维护"]
    C1 --> C1c["终止"]

    D --> D1["BUILD-MAX-HEAP: O(n)"]
    D --> D2["n-1 次 MAX-HEAPIFY: O(n lg n)"]
    D --> D3["总计: O(n lg n)"]

    E --> E1["原地排序 (in-place)"]
    E --> E2["不稳定排序"]
    E --> E3["比较排序 - 渐近最优"]

核心思想

核心思路

HEAPSORT 巧妙地利用了最大堆的性质:堆顶元素始终是最大值。算法的基本策略是:

  1. 先将整个数组构建为最大堆
  2. 将堆顶最大元素与数组末尾交换——最大元素就位
  3. 缩小堆的规模(排除已就位的元素),对新的堆顶执行 MAX-HEAPIFY 恢复堆性质
  4. 重复步骤2-3,直到堆中只剩一个元素

这个过程就像 repeatedly 从一堆石头中挑出最大的那块放到最终位置,每次挑完后重新整理剩下的石头。

HEAPSORT 伪代码

算法执行流程

  1. 调用 BUILD-MAX-HEAP(A, n) 将数组构建为最大堆
  2. i = n 开始,将堆顶 A[1](当前最大值)与 A[i] 交换,最大值就位
  3. A.heap-size 减 1,将已就位的元素排除出堆
  4. 对新的堆顶调用 MAX-HEAPIFY(A, 1) 恢复堆性质
  5. i 递减 1,重复步骤 2-4,直到 i = 1,排序完成
flowchart TD
    A["调用 BUILD-MAX-HEAP(A, n)"] --> B["i = n"]
    B --> C{"i >= 2?"}
    C -- 是 --> D["交换 A[1] 与 A[i]<br/>最大值就位"]
    D --> E["A.heap-size = A.heap-size - 1"]
    E --> F["调用 MAX-HEAPIFY(A, 1)<br/>恢复堆性质"]
    F --> G["i = i - 1"]
    G --> C
    C -- 否 --> H["排序完成"]
HEAPSORT(A, n)
1  BUILD-MAX-HEAP(A, n)
2  for i = n downto 2
3      exchange A[1] with A[i]
4      A.heap-size = A.heap-size - 1
5      MAX-HEAPIFY(A, 1)

HEAPSORT

输入: 数组 (无序) 输出: 原地排序为非降序序列

算法步骤:

  1. 建堆: 调用 BUILD-MAX-HEAP(A, n),将数组转换为最大堆
  2. 排序循环: 递减到
    • (当前最大元素)与 交换,最大元素就位
    • A.heap-size 减1,将 排除出堆
    • 对新的堆顶调用 MAX-HEAPIFY(A, 1),恢复堆性质

循环不变式与正确性证明

循环不变式

在 for 循环(第2-5行)每次迭代开始时:

  • 子数组 是一个包含 最小的 个元素最大堆
  • 子数组 包含 最大的 个元素,且已排好序

初始化(Initialization):

  • 在第一次迭代之前,
  • 刚执行完 BUILD-MAX-HEAP 是一个最大堆,包含所有 个元素
  • 子数组 为空,空数组自然是有序的
  • 循环不变式成立

维护(Maintenance):

  • 每次迭代开始时, 是最大堆, 已排序且包含最大的 个元素
  • 第3行将 (堆中的最大元素,也是 中的最大元素)与 交换
  • 交换后, 现在存放的是 中的最大值,它不超过 中的任何元素(因为 包含更大的元素)
  • 第4行将 heap-size 减1,将 排除出堆
  • 此时根节点的子树仍然是最大堆,但根节点可能违反堆性质
  • 第5行 MAX-HEAPIFY(A, 1) 恢复堆性质,使 成为最大堆
  • 现在 包含最大的 个元素且已排序
  • for 循环将 递减1,重新建立循环不变式

终止(Termination):

  • 循环在 时终止(因为循环条件是
  • 由循环不变式: 是包含最小1个元素的最大堆(平凡成立), 包含最大的 个元素且已排序
  • 因此整个数组 已排好序
  • 算法正确性得证

运行时间分析

时间复杂度

HEAPSORT 的运行时间由两部分组成:

  1. BUILD-MAX-HEAP(第1行): 耗时 (见6.3节的精确分析)
  2. for 循环(第2-5行): 共执行 次迭代
    • 每次迭代中,MAX-HEAPIFY(A, 1) 在一个大小递减的堆上运行
    • 次迭代时堆的大小为 MAX-HEAPIFY 耗时
    • 总时间:

总运行时间: ====

这是比较排序的理论下界(见第8章),因此 HEAPSORT 是渐近最优的比较排序算法。


补充理解与拓展

堆排序 vs 快速排序 vs 归并排序——工程视角的全面对比

比较维度堆排序快速排序归并排序
最坏时间O(n lg n)O(n^2)O(n lg n)
平均时间O(n lg n)O(n lg n)O(n lg n)
空间O(1) 原地O(lg n) 栈空间O(n) 额外空间
稳定性不稳定不稳定稳定
缓存友好性差(跳跃访问)好(局部访问)中等(顺序合并)
实际速度最慢最快中等
并行化困难较易容易

为什么快速排序在实践中通常最快?

  1. 缓存局部性:快速排序的分区操作在数组的连续区域进行交换,充分利用CPU缓存的空间局部性和时间局部性。堆排序的HEAPIFY操作在树的不同层级间跳跃,缓存未命中率高(LaMarca & Ladner, 1996)
  2. 分支预测:快速排序的内循环简单(比较+交换),CPU分支预测器可以高效预测
  3. 常数因子:快速排序的内循环操作更少,每次迭代只需一次比较和最多一次交换

堆排序的独特优势

  • 最坏情况O(n lg n)保证,适合实时系统(如航空航天、医疗设备)中对延迟有上界要求的场景
  • O(1)额外空间,在内存受限的嵌入式系统中优势明显
  • Java标准库的Arrays.sort()对基本类型使用双轴快速排序(优化的快速排序),对对象类型使用TimSort(归并排序+插入排序的混合),并未使用堆排序——这从侧面反映了堆排序在实际排序中的地位

来源:LaMarca & Ladner, “The Influence of Caches on the Performance of Sorting”, SODA 1996; DesignGurus.io; 极客时间《数据结构与算法之美》

堆排序的缓存不敏感变体与现代研究

传统堆排序的缓存性能差是一个长期存在的问题,研究者提出了多种改进方案:

  1. Cache-oblivious heapsort(2000年代):Arne Andersson提出的缓存不敏感堆排序,通过特殊的内存布局使算法在任何缓存大小下都表现良好,无需知道具体的缓存参数
  2. Bottom-up heapsort(1993年):Ingo Wegener提出的改进版本,HEAPIFY操作从叶节点向上搜索而非从根节点向下,减少了比较次数。实验表明可减少约50%的比较操作
  3. Mergesort-Heapsort混合:对于大数据集,先用堆排序处理小块,再用归并排序合并,兼顾缓存友好性和原地性
  4. Introsort(1997年):Musser提出的内省排序,结合了快速排序、堆排序和插入排序——先用快速排序,当递归深度超过 时切换到堆排序防止退化,对小分区切换到插入排序。C++ STL的std::sort就采用此策略

这些改进说明,虽然纯堆排序在缓存性能上有劣势,但通过算法工程(algorithm engineering)可以显著缩小差距。


易混淆点与辨析

误区:HEAPSORT 产生的是降序排列

错误理解: “HEAPSORT 使用最大堆,每次取出最大元素放到末尾,所以产生的是降序排列”

正确理解: HEAPSORT 确实使用最大堆,每次取出最大元素放到数组末尾(从 开始向前填充)。随着算法进行,数组末尾积累的是越来越小的元素—— 是最大值, 是次大值,依此类推。最终数组 非降序排列(升序)。

记忆方法: 最大堆的最大值被”沉”到数组最右端,次大值被”沉”到倒数第二位…最终从左到右就是从小到大。

误区:HEAPSORT 是稳定排序

错误理解: “HEAPSORT 是 的高效排序,应该是稳定排序”

正确理解: HEAPSORT 是不稳定排序。在交换 的过程中,可能改变相等元素的相对顺序。

具体例子: 假设数组中有两个相等的元素 之前出现。在建堆过程中, 可能被提升到 的上方。在排序时, 先被取出放到数组末尾, 后被取出放到 前面——它们的相对顺序被颠倒了。

对比: 归并排序 是稳定排序,而 HEAPSORT 和快速排序都是不稳定排序。稳定性在选择排序算法时是一个重要考量因素。


习题精选

题号题目描述难度
6.4-1模仿图6.4,展示 HEAPSORT 在数组 上的操作过程
6.4-2使用循环不变式论证 HEAPSORT 的正确性⭐⭐
6.4-3HEAPSORT 在已按升序排列的数组上运行时间是多少?在已按降序排列的数组上呢?⭐⭐
6.4-4证明 HEAPSORT 的最坏情况运行时间是 ⭐⭐
6.4-5证明当所有元素互不相同时,HEAPSORT 的最好情况运行时间是 ⭐⭐⭐

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 4Heaps and Heap Sorthttps://www.youtube.com/watch?v=B7hVxCmfPtM完整的堆排序讲解
Abdul BariHeap Sort Algorithmhttps://www.youtube.com/watch?v=HqPJF2L5h9U逐步动画演示,含建堆与排序全过程
WilliamFisetHeapsort Examplehttps://www.youtube.com/watch?v=6cGzGDOKDxk排序算法系列,含示例演示
NeetCodeHeap / Priority Queuehttps://www.youtube.com/watch?v=XEmy13g1Qxc实战视角的堆操作

教材原文

CLRS 第4版 6.4节原文

The heapsort algorithm, given by the procedure HEAPSORT, starts by calling the BUILD-MAX-HEAP procedure to build a max-heap on the input array . Since the maximum element of the array is stored at the root , HEAPSORT can place it into its correct final position by exchanging it with . If the procedure then discards node from the heap—and it can do so by simply decrementing —the children of the root remain max-heaps, but the new root element might violate the max-heap property. To restore the max-heap property, the procedure just calls MAX-HEAPIFY(A, 1), which leaves a max-heap in . The HEAPSORT procedure then repeats this process for the max-heap of size down to a heap of size 2.

The HEAPSORT procedure takes time, since the call to BUILD-MAX-HEAP takes time and each of the calls to MAX-HEAPIFY takes time.


参见Wiki

  • 堆排序 — 基于最大堆的原地排序算法

第06章-堆排序 堆排序算法