mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1132 字
3 分钟
堆排序
2026-06-09 03:42:57

1. 算法思想#

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值总是小于或大于它的父节点。

根据父子节点之间的关系,堆可以分为两类:

大根堆与小根堆

  • 大根堆:每个节点的值均大于或等于其左右孩子节点的值。
  • 小根堆:每个节点的值均小于或等于其左右孩子节点的值。

2. 堆的数学性质#

若对堆中的节点按照层序遍历的方式进行编号,则可将其结构映射到一维数组中。

0 起始编号#

若从 00 开始编号,则节点 ii 的左子节点为 2i+12i + 1,右子节点为 2i+22i + 2。此时大根堆和小根堆满足以下关系:

{大根堆:nums[i]nums[2i+1] 且 nums[i]nums[2i+2]小根堆:nums[i]nums[2i+1] 且 nums[i]nums[2i+2]\begin{cases} \text{大根堆:} \text{nums}[i] \ge \text{nums}[2i + 1] \text{ 且 } \text{nums}[i] \ge \text{nums}[2i + 2] \\ \text{小根堆:} \text{nums}[i] \le \text{nums}[2i + 1] \text{ 且 } \text{nums}[i] \le \text{nums}[2i + 2] \end{cases}

1 起始编号#

若从 11 开始编号,则节点 ii 的左子节点为 2i2i,右子节点为 2i+12i + 1。此时大根堆和小根堆满足以下关系:

{大根堆:nums[i]nums[2i] 且 nums[i]nums[2i+1]小根堆:nums[i]nums[2i] 且 nums[i]nums[2i+1]\begin{cases} \text{大根堆:} \text{nums}[i] \ge \text{nums}[2i] \text{ 且 } \text{nums}[i] \ge \text{nums}[2i + 1] \\ \text{小根堆:} \text{nums}[i] \le \text{nums}[2i] \text{ 且 } \text{nums}[i] \le \text{nums}[2i + 1] \end{cases}

3. 构造大根堆#

假设有一个待排序的数组 nums = [5, 2, 1, 9, 6, 8],我们可将其构造成一个完全二叉树:

完全二叉树结构

要将一个完全二叉树构造成一个大顶堆,一个高效的实现方式是从最后一个非叶子节点开始,自底向上、从右往左进行调整。

若数组长度为 nn,从 00 开始编号,第一个非叶子节点的下标为 n/21n / 2 - 1。对于以该非叶子节点为根的子树,调整规则如下:

  1. 如果该节点大于或等于左右子节点,则无需调整。
  2. 否则,将该节点与左右子节点中较大者进行交换,并从被交换的子节点位置出发继续向下调整,直到子树满足堆性质。

具体建堆步骤如下:

步骤一#

第一个非叶子节点为 22,其值为 11。由于其左子节点 88 较大,交换两者:

建堆步骤一

步骤二#

第二个非叶子节点为 11,其值为 22。因为其左右子节点分别为 9966,左子节点较大且大于 22,交换两者:

建堆步骤二

步骤三#

第三个非叶子节点为 00,其值为 55。因为其左右子节点分别为 9988,左子节点较大且大于 55,交换两者:

建堆步骤三第一步

交换后发现子树堆性质被打破(此时该位置的值 55 小于右子节点 66),需要继续对以该节点为根的子树进行调整:

建堆步骤三第二步

此时建堆完毕,得到大根堆 nums = [9, 6, 8, 2, 5, 1]

大根堆构建完成


4. 排序过程#

建立好大根堆后,排序的步骤如下:

  1. 将堆顶元素(最大值)与堆尾元素交换,此时最大值被放置到数组末尾。
  2. 缩小堆的有效范围,将剩余的 n1n - 1 个元素重新调整为大根堆。
  3. 重复该过程,直到堆中仅剩一个元素。

提取最大元素#

将堆顶元素 99 与末尾元素 11 交换,并将 99 移出堆的范围:

提取最大元素并调整

对剩余元素重新调整,使其满足大根堆性质:

堆重建

提取次大元素#

将当前的堆顶 88 与堆尾元素 22 交换,并将 88 移出堆的范围:

提取次大元素

提取第三大元素#

将堆顶 66 与尾部的 11 交换:

提取第三大元素

提取第四大元素#

将堆顶 55 与尾部的 22 交换:

提取第四大元素

提取第五大元素#

将堆顶 22 与尾部的 11 交换,此时堆中仅剩一个元素,排序完成:

提取第五大元素

最终得到升序排列的数组:

排序完成


5. Java 实现#

下面给出两种常见的实现方式。

5.1 从下标 0 开始建堆#

class Solution {
public int[] sortArray(int[] nums) {
int n = nums.length;
// 自底向上构建大根堆
for (int root = n / 2 - 1; root >= 0; root--) {
maxHeapify(nums, root, n);
}
// 依次交换堆顶与未排序部分末尾,并重新调整
for (int tail = n - 1; tail > 0; tail--) {
int temp = nums[0];
nums[0] = nums[tail];
nums[tail] = temp;
maxHeapify(nums, 0, tail);
}
return nums;
}
private void maxHeapify(int[] heap, int root, int heapSize) {
int largest = root;
while (true) {
int left = 2 * root + 1;
int right = 2 * root + 2;
if (left < heapSize && heap[largest] < heap[left]) {
largest = left;
}
if (right < heapSize && heap[largest] < heap[right]) {
largest = right;
}
if (largest != root) {
int temp = heap[root];
heap[root] = heap[largest];
heap[largest] = temp;
root = largest;
} else {
break;
}
}
}
}

5.2 从下标 1 开始建堆#

为了更方便地通过索引计算父子关系,可在数组最前面插入一个占位元素,使得有效数据下标从 11 开始。

class Solution {
public int[] sortArray(int[] nums) {
int n = nums.length;
int[] arr = new int[n + 1];
System.arraycopy(nums, 0, arr, 1, n); // 头部保留 arr[0] = 0 作为占位元素
// 构建大根堆,首个非叶子节点为 n / 2
for (int i = n / 2; i > 0; i--) {
maxHeapify(arr, i, n);
}
// 排序过程
for (int j = n; j > 0; j--) {
int temp = arr[1];
arr[1] = arr[j];
arr[j] = temp;
maxHeapify(arr, 1, j - 1);
}
// 复制回原数组或生成新数组返回
int[] result = new int[n];
System.arraycopy(arr, 1, result, 0, n);
return result;
}
private void maxHeapify(int[] arr, int i, int end) {
int j = 2 * i; // 左子节点
while (j <= end) {
if (j + 1 <= end && arr[j + 1] > arr[j]) {
j++; // 选择左右子节点中的较大者
}
if (arr[i] < arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i = j;
j = 2 * i;
} else {
break;
}
}
}
}

6. 复杂度分析#

  • 时间复杂度O(nlogn)O(n \log n),其中 nn 为数组的长度。初始化建堆的时间复杂度为 O(n)O(n),之后每次调整的时间复杂度为 O(logn)O(\log n),共进行 n1n - 1 次调整。
  • 空间复杂度:对于从下标 00 开始的实现,空间复杂度为 O(1)O(1),属于原地排序;对于从下标 11 开始的实现,由于创建了辅助数组 arr,会产生额外的 O(n)O(n) 空间开销。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时

目录