乐于分享
好东西不私藏

Java源码中的排序算法(三)- HeapSort

Java源码中的排序算法(三)- HeapSort

上一篇讲 DualPivotQuicksort(双轴快排)时提到它的智能算法切换,通过源码可以看到它的切换选择有多个,今天我们讲一个当检测到程序有 “恶化倾向(当时间复杂度可能变为O(n²) )” ,作为保底策略启用的堆排序。

final class DualPivotQuicksort {  /**   * Threshold of mixed insertion sort is incremented by this value.   * 混合插入排序的阈值通过此值进行递增。   */  private static final int DELTA = 3 << 1//常量 6  /**   * Max recursive partitioning depth before using heap sort.   * 在使用堆排序之前的最大递归分区深度。   */  private static final int MAX_RECURSION_DEPTH = 64 * DELTA;  staticvoidsort(Sorter sorter, int[] a, int bits, int low, int high){    // 省略部分代码...    /*     * Switch to heap sort if execution     * time is becoming quadratic.(时间复杂度正在变为O(n²)。)     */    if ((bits += DELTA) > MAX_RECURSION_DEPTH) {      heapSort(a, low, high);      return;    }    // 省略部分代码...  }}

这么做的好处

  • 防止栈溢出:过深的递归可能导致 StackOverflowError
  • 性能保护:当递归深度过大时,快排可能退化为 O(n²) 复杂度
  • 保证稳定性:通过切换到堆排序等方法维持 O(n log n) 的时间复杂度

这正是“稳”的工程体现,今天就聚焦这个低调排序算法 ——— 堆排序,逐步拆解学习,让我们开始吧


一、堆排序工作过程:建堆 + 抽顶

先看一段JDK风格堆排序代码(简化自 java.util.DualPivotQuicksort内部实现):

private static void heapSort(int[] a, int low, int high) {  // STEP 1:自底向上建大顶堆(从最后一个非叶子节点开始)  for (int k = (low + high) >>> 1; k > low; ) {    pushDown(a, --k, a[k], low, high);  }  // STEP 2:反复“抽顶”:堆顶最大值 → 末尾,重建堆  while (--high > low) {    int max = a[low];                // 保存堆顶(当前最大值)    pushDown(a, low, a[high], low, high); // 末尾元素上位,下沉调整    a[high] = max;                   // 最大值归位到已排序区  }}private static void pushDown(int[] a, int p, int value, int low, int high) {  for (int k ;; a[p] = a[p = k]) {    k = (p << 1) - low + 2// 计算右孩子索引    if (k > high) break;               // 无孩子,停止    if (k == high || a[k] < a[k - 1]) --k; // 选更大的孩子    if (a[k] <= value) break;          // 父节点不小于子节点,停止  }  a[p] = value;}

实例演示:排序数组 [4,10,3,5,1]

初始状态(二叉树表示):

      4    /   \  10     3 /  \5    1

步骤1:建大顶堆

  • 从索引 (0+4)/2=2 向前遍历(索引2为叶子节点,跳过)
  • 调整索引1(值10):已满足堆性质
  • 调整索引0(值4):
  • 4 与子节点10交换 → [10,4,3,5,1]
  • 4 与子节点5交换 → [10,5,3,4,1]
      4    /   \  10     3 /  \5    1

步骤2:交换堆顶 + 重建堆

轮次
操作
数组变化
说明
1
交换10↔1
[1,5,3,4,10]
10归位末尾
重建堆
[5,4,3,1,10]
1下沉至合适位置
2
交换5↔1
[1,4,3,5,10]
5归位
重建堆
[4,1,3,5,10]
最终
[1,3,4,5,10]
排序完成

源码关键点解析

  • 索引计算: k=(p<<1)-low+2 精准计算右孩子索引,支持任意子数组区间( low非0场景)
  • 下沉优化: value暂存待调整值,最后一次性赋值,减少内存写入次数
  • 边界处理: k==high 判断巧妙处理“仅存在左孩子”的边界情况
  • 高效建堆:从 (low+high)/2 开始向前遍历,跳过叶子节点无效操作

二、性能优势:为什么说它“稳如泰山”?

维度
堆排序
快速排序
归并排序
最坏时间复杂度
O(n log n)
O(n²)
O(n log n)
空间复杂度
O(1)
O(log n)
O(n)
稳定性
不稳定
不稳定
稳定
缓存友好性
一般
优秀
一般
适用场景
内存敏感+最坏保障
通用首选
稳定性要求高

核心优势

  1. 最坏情况有保障:实时系统、金融交易等绝不允许性能雪崩的场景
  2. 内存零负担:嵌入式设备、IoT环境(原地排序,无需额外数组)
  3. Top-K问题高效解:建大小为K的堆,时间复杂度O(n log K),远优于全排序O(n log n)

示例:从1亿条日志中找访问量最高的10个IP使用最小堆维护Top 10,内存仅占10个元素,效率碾压全排序。


三、适用场景决策指南

推荐使用场景

  • 内存极度受限环境:嵌入式系统、单片机(O(1)空间)
  • 需要最坏性能保证:航天控制、高频交易系统(拒绝O(n²)风险)
  • Top-K问题:实时榜单、推荐系统(用堆而非全排序)
  • 外部排序辅助:数据库多路归并中维护K路最小值

避坑指南

  • 数据量小(<50):插入排序更快(常数因子小)
  • 需要稳定排序:选择TimSort或归并排序(如按时间二次排序需保留原始顺序)
  • 数据基本有序:快排或TimSort更优(堆排序无局部性优势)

💡 感谢你看完这篇内容,这是我自己在工作学习中遇到的case,做一些简单的 究,并总结经验,如有遗漏或不合理的地方,欢迎你提出问题,让我们一起探索

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Java源码中的排序算法(三)- HeapSort

评论 抢沙发

3 + 9 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮