Java 堆排序(Heap Sort)详解教程
原文:CSDN(历史文章导入,当前状态为草稿)
一、什么是堆(Heap)?
Section titled “一、什么是堆(Heap)?”堆是一种完全二叉树(complete binary tree),分为:
- 大顶堆(max heap):每个节点的值 ≥ 子节点。
- 小顶堆(min heap):每个节点的值 ≤ 子节点。
在堆排序中,我们使用 大顶堆 来进行 升序排序。
二、堆的数组存储结构
Section titled “二、堆的数组存储结构”堆通常不用链表存储,而是直接放在数组里。
节点下标关系如下(假设数组下标从 0 开始):
| 节点 | 公式 | 说明 |
|---|---|---|
| 父节点 | i | 当前节点索引 |
| 左子节点 | 2*i + 1 | |
| 右子节点 | 2*i + 2 |
示例:数组 [4, 6, 8, 5, 9]
Section titled “示例:数组 [4, 6, 8, 5, 9]”树形结构如下:
4(0) / \ 6(1) 8(2) / \ 5(3) 9(4)三、堆排序核心思路
Section titled “三、堆排序核心思路”核心思想一句话:
Section titled “核心思想一句话:”不断取出堆顶(最大值),放到数组末尾,然后重建大顶堆。
也就是:
- 先把数组调整成「大顶堆」;
- 交换堆顶元素(最大值)与数组末尾;
- 堆大小减 1,重新调整为大顶堆;
- 重复直到数组有序。
四、为什么从 n/2 - 1 开始建堆?
Section titled “四、为什么从 n/2 - 1 开始建堆?”这个是很多人疑惑的地方👇
对于数组长度 n:
-
叶子节点的下标范围是
[n/2, n-1]。 -
因为在堆的结构中:
左孩子 = 2*i + 1+ 1所以只要
2*i + 1 < n,i就是一个非叶子节点。
推导:
2*i + 1 < n→ i < (n - 1)/2→ i_max = floor((n - 1)/2)因此:
最后一个非叶子节点的下标 = n/2 - 1。
数组长度 n = 5
→ 最后一个非叶子节点 = 5/2 - 1 = 1
(节点索引 1:左=3, 右=4 ✅)
因为从 n/2 开始的节点,它们都已经是叶子,没有孩子要调整。
所以建堆时:
for (int i = n/2 - 1; i >= 0; i--) { heapify(arr, n, i);}从下往上建堆,子树先调整好,父节点才能正确“下沉”。
这就回答了你的另一个疑问👇
五、为什么不能从根节点(0)开始建堆?
Section titled “五、为什么不能从根节点(0)开始建堆?”因为如果你从上往下建:
- 根节点下沉时会依赖子节点的堆结构;
- 但此时子节点可能还没被调整好。
比如:
4 / \ 9 8当根节点 4 尝试下沉时,无法正确判断应该下沉到哪里,因为左右子树还没整理成堆。
所以必须从最后一个非叶子节点开始,自底向上堆化。
六、堆化(heapify)的本质
Section titled “六、堆化(heapify)的本质”堆化的作用是:
让以当前节点
i为根的子树满足大顶堆性质。
伪代码逻辑:
Section titled “伪代码逻辑:”void heapify(int[] arr, int n, int i) { int largest = i; // 假设当前节点最大 int left = 2*i + 1; // 左孩子 int right = 2*i + 2; // 右孩子
// 左孩子比当前节点大 if (left < n && arr[left] > arr[largest]) largest = left;
// 右孩子比当前最大的大 if (right < n && arr[right] > arr[largest]) largest = right;
// 如果最大值不是自己,交换并递归堆化 if (largest != i) { swap(arr, i, largest); heapify(arr, n, largest); }}⚙️ 注意:heapify 是递归的。
它会让当前节点「下沉」到正确位置,直到子树满足大顶堆。
七、完整堆排序代码
Section titled “七、完整堆排序代码”public class HeapSort { public static void heapSort(int[] arr) { int n = arr.length;
// 1️⃣ 构建初始大顶堆 for (int i = n / 2 - 1; i >= 0; i--) { heapify(arr, n, i); }
// 2️⃣ 依次取出堆顶(最大值),放到数组末尾 for (int i = n - 1; i > 0; i--) { swap(arr, 0, i); // 最大值移到末尾 heapify(arr, i, 0); // 重新堆化剩余部分 } }
private static void heapify(int[] arr, int n, int i) { int largest = i; int left = 2 * i + 1; int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i) { swap(arr, i, largest); heapify(arr, n, largest); } }
private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
// 测试 public static void main(String[] args) { int[] arr = {4, 6, 8, 5, 9}; heapSort(arr); System.out.println(Arrays.toString(arr)); // [4, 5, 6, 8, 9] }}八、为什么要“依次取出堆顶并重建堆”?
Section titled “八、为什么要“依次取出堆顶并重建堆”?”因为堆顶元素是整个堆中最大的。
- 把堆顶和末尾元素交换 → 最大元素放到数组末尾;
- 缩小堆的范围(排除最后一个最大值);
- 重建堆(保持剩下的部分仍然是大顶堆)。
如此反复,每次都确定一个新的“最大值”,放到正确位置,最终数组有序。
九、时间复杂度分析
Section titled “九、时间复杂度分析”| 阶段 | 操作 | 复杂度 |
|---|---|---|
| 建堆 | 从底向上 heapify | O(n) |
| 排序 | 取堆顶 + 重建堆 n 次 | O(n log n) |
| 总体 | O(n log n) |
空间复杂度:O(1),属于原地排序。
十、完整流程图
Section titled “十、完整流程图”输入数组 → [4,6,8,5,9]↓建堆↓[9,6,8,5,4]↓交换堆顶与末尾 → [4,6,8,5,9]↓重建堆 → [8,6,4,5,9]↓重复…↓最终有序:[4,5,6,8,9]| 步骤 | 说明 | 核心 |
|---|---|---|
| 1 | 从 n/2 - 1 开始建堆 |
因为这是最后一个非叶子节点 |
| 2 | 从下往上建堆 | 确保子树先堆化 |
| 3 | 每次交换堆顶和末尾 | 把当前最大元素放对位置 |
| 4 | 重建堆 | 保证剩余部分仍是大顶堆 |
| 5 | 重复 n 次 | 数组排序完成 |