1. 算法思想
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值总是小于或大于它的父节点。
根据父子节点之间的关系,堆可以分为两类:

- 大根堆:每个节点的值均大于或等于其左右孩子节点的值。
- 小根堆:每个节点的值均小于或等于其左右孩子节点的值。
2. 堆的数学性质
若对堆中的节点按照层序遍历的方式进行编号,则可将其结构映射到一维数组中。
0 起始编号
若从 开始编号,则节点 的左子节点为 ,右子节点为 。此时大根堆和小根堆满足以下关系:
1 起始编号
若从 开始编号,则节点 的左子节点为 ,右子节点为 。此时大根堆和小根堆满足以下关系:
3. 构造大根堆
假设有一个待排序的数组 nums = [5, 2, 1, 9, 6, 8],我们可将其构造成一个完全二叉树:

要将一个完全二叉树构造成一个大顶堆,一个高效的实现方式是从最后一个非叶子节点开始,自底向上、从右往左进行调整。
若数组长度为 ,从 开始编号,第一个非叶子节点的下标为 。对于以该非叶子节点为根的子树,调整规则如下:
- 如果该节点大于或等于左右子节点,则无需调整。
- 否则,将该节点与左右子节点中较大者进行交换,并从被交换的子节点位置出发继续向下调整,直到子树满足堆性质。
具体建堆步骤如下:
步骤一
第一个非叶子节点为 ,其值为 。由于其左子节点 较大,交换两者:

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

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

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

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

4. 排序过程
建立好大根堆后,排序的步骤如下:
- 将堆顶元素(最大值)与堆尾元素交换,此时最大值被放置到数组末尾。
- 缩小堆的有效范围,将剩余的 个元素重新调整为大根堆。
- 重复该过程,直到堆中仅剩一个元素。
提取最大元素
将堆顶元素 与末尾元素 交换,并将 移出堆的范围:

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

提取次大元素
将当前的堆顶 与堆尾元素 交换,并将 移出堆的范围:

提取第三大元素
将堆顶 与尾部的 交换:

提取第四大元素
将堆顶 与尾部的 交换:

提取第五大元素
将堆顶 与尾部的 交换,此时堆中仅剩一个元素,排序完成:

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

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 开始建堆
为了更方便地通过索引计算父子关系,可在数组最前面插入一个占位元素,使得有效数据下标从 开始。
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. 复杂度分析
- 时间复杂度:,其中 为数组的长度。初始化建堆的时间复杂度为 ,之后每次调整的时间复杂度为 ,共进行 次调整。
- 空间复杂度:对于从下标 开始的实现,空间复杂度为 ,属于原地排序;对于从下标 开始的实现,由于创建了辅助数组
arr,会产生额外的 空间开销。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时


















