-📖 ​​故事:体育课选拔队长(堆排序原理)​

72 阅读4分钟

想象你是体育老师,要把一群身高不同的学生按身高排队(升序)。但直接排序太累,你想了个妙招:

  1. ​建“高个子金字塔”(建大顶堆)​​:

    • 让学生站成​​完全二叉树​​队形(数组存储),要求:​​父节点必须 ≥ 子节点​​(大顶堆)。

    • 从​​最后一个非叶子节点​​(倒数第二排)开始调整:

      • 比如节点A(身高160)的左右孩子分别是155和175,175更高 → 让175和A交换位置。
      • 接着检查前一个非叶子节点,直到根节点。
    • ​结果​​:根节点(堆顶)是最高学生(175),但其他节点不一定有序。

  2. ​选拔队长 + 重新整队(排序阶段)​​:

    • ​第1轮​​:堆顶(175)和队尾交换 → 175站到末尾(已有序)。
    • 剩余学生中,根节点(原倒数第二高的学生)可能破坏堆结构 → 重新调整为大顶堆(根节点下沉)。
    • ​重复​​:新堆顶(如165)再和倒数第二位置交换 → 165归位,继续调整……

​最终效果​​:每次交换堆顶都让一个高个子归位,队尾逐渐有序,队前保持大顶堆结构。


⚙️ ​​Java代码实现(逐行解析)​

java
Copy
public class HeapSort {
    // 堆排序入口
    public static void heapSort(int[] arr) {
        // 1. 构建初始大顶堆(从最后一个非叶子节点开始)
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length); // 调整子树
        }
        // 2. 排序:交换堆顶与末尾 + 调整剩余堆
        for (int j = arr.length - 1; j > 0; j--) {
            swap(arr, 0, j);         // 堆顶(最大值)放到末尾
            adjustHeap(arr, 0, j);    // 剩余部分重新调整
        }
    }

    // 调整堆(核心:父节点下沉)
    private static void adjustHeap(int[] arr, int parent, int len) {
        int temp = arr[parent]; // 暂存父节点值
        for (int child = 2 * parent + 1; child < len; child = 2 * child + 1) {
            // 找最大子节点:右孩子存在且更大 → 选右孩子
            if (child + 1 < len && arr[child] < arr[child + 1]) {
                child++;
            }
            // 子节点 > 父节点?父节点下沉!
            if (arr[child] > temp) {
                arr[parent] = arr[child]; // 子节点覆盖父节点
                parent = child;           // 父指针下移到子节点
            } else {
                break; // 父节点 >= 子节点 → 无需调整
            }
        }
        arr[parent] = temp; // 父节点找到最终位置
    }

    // 交换元素
    private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args) {
        int[] heights = {160, 155, 175, 170, 165};
        heapSort(heights);
        System.out.println("按身高排队: " + Arrays.toString(heights));
        // 输出: [155, 160, 165, 170, 175]
    }
}

🔍 ​​关键步骤图解(以数组 [4, 10, 3, 5, 1] 为例)​

  1. ​初始建堆​​:

    Copy
         4              → 调整节点11010
        / \                子节点5>4? 交换      / \
       10  3              → 最终大顶堆:      5   3
      / \               [10, 5, 3, 4, 1]    / \
     5   1                                  4   1
    
  2. ​排序阶段​​:

    • ​第1轮​​:堆顶10和末尾1交换 → 数组变 [1, 5, 3, 4, 10](10归位)。

    • 调整剩余堆 [1, 5, 3, 4] → 父节点1下沉,5上浮 → 新堆顶5。

    • ​第2轮​​:5和4交换 → [4, 1, 3, 5, 10](5归位)。

    • 继续调整直到全部有序


📊 ​​堆排序性能与特点​

​特性​​说明​
⏱️ 时间复杂度​O(n log n)​​(建堆O(n) + n次调整O(log n))45
📦 空间复杂度​O(1)​​(原地排序,无需额外空间)79
⚠️ 稳定性​不稳定​​(父子交换可能破坏相同值顺序,如 [5₁, 5₂, 1] → 5₂可能先于5₁)9
💡 适用场景大规模数据、内存受限场景(如嵌入式系统)、Top K问题(如找前10名)510

❓ ​​常见疑问解答​

  1. ​为什么升序用大顶堆?​
    大顶堆的堆顶是最大值,每次交换到末尾,​​队尾逐渐形成有序区​​(升序)。若用小顶堆,堆顶是最小值,交换后需放队首,实现更复杂

  2. ​为什么从 n/2-1 开始建堆?​
    完全二叉树中,​​最后一个非叶子节点索引 = n/2-1​(n为长度)。从此处倒序调整,可减少无效比较

  3. ​堆排序 vs 快速排序?​

    • ​堆排序​​:时间复杂度稳定O(n log n),但​​缓存命中率低​​(父子节点内存不连续)

    • ​快速排序​​:平均O(n log n),但最坏O(n²);​​缓存友好但递归栈占用空间​


💎 ​​总结​

堆排序 = ​​“堆化 + 交换下沉”​​:

  1. ​建堆​​:从后向前调整非叶子节点,构建大顶堆;
  2. ​排序​​:堆顶与末尾交换 → 最大值归位 → 剩余部分重新堆化。

🌟 ​​一句话精髓​​:像选拔队长一样,每次选出最高者放到队尾,剩余队伍重新整队,直到全员有序!

掌握此算法,你不仅能处理海量数据排序,还能高效解决Top K问题(如“10万订单中找金额最高的前10笔”)