堆排序是一种基于 "堆数据结构" 的排序算法,核心逻辑是 "利用堆的特性(父节点优先级高于子节点)筛选出最大值 / 最小值,逐步构建有序序列"。
堆的定义与特性
1. 堆的分类
-
大顶堆(Max Heap):每个父节点的值 ≥ 其左右子节点的值(根节点是最大值);
-
小顶堆(Min Heap) :每个父节点的值 ≤ 其左右子节点的值(根节点是最小值);堆排序默认用 大顶堆(升序排序),小顶堆可用于降序排序。
2.堆的存储方式(数组映射)
堆通常用 数组 存储(完全二叉树的特性适配数组索引),无需额外存储指针,空间效率高。假设数组索引从 0 开始,对于任意节点i:
-
左子节点索引:
2i + 1 -
右子节点索引:
2i + 2 -
父节点索引:
(i - 1) // 2(整数除法)
3.堆化(Heapify):修复堆结构
功能:当某个节点破坏堆序性(如父节点 <子节点)时,将其 "下沉" 到正确位置,确保子树满足堆特性
输入:数组、当前节点索引、堆的大小(避免越界);
步骤(大顶堆为例):
- 假设当前节点
i是最大值(初始候选);- 找到
i的左、右子节点,比较三者大小,更新最大值索引max_idx;- 若
max_idx != i(说明子节点更大,破坏堆序):- 交换
arr[i]和arr[max_idx];- 递归对
max_idx位置的节点堆化(交换后该子节点可能破坏子树堆结构);
时间复杂度:O(log n)(堆的高度为log n,节点下沉最多需log n次比较)。
构建初始堆
功能:将无序数组转化为大顶堆(或小顶堆);
核心逻辑:从 最后一个非叶子节点 开始,向前依次对每个节点执行堆化操作;
最后一个非叶子节点索引:
(n // 2) - 1(n是数组长度,叶子节点无需堆化);时间复杂度:
O(n)(看似n log n,实际多数节点深度小,数学推导后为O(n))。
堆排序的完整流程(升序排序)
堆排序的核心思想是 "反复提取堆顶最大值,构建有序序列",步骤如下:
-
构建初始大顶堆:将无序数组转化为大顶堆(根节点是最大值);
-
提取堆顶元素 :交换根节点(索引 0)和堆的最后一个元素(索引
n-1),此时最大值被放到数组末尾(有序区); -
缩小堆范围:堆的大小减 1(有序区不再参与堆操作);
-
堆化修复:对新的根节点执行堆化操作,重新构建大顶堆;
-
重复步骤 2-4:直到堆的大小为 1,数组完全有序。
流程可视化(以数组[4, 6, 8, 5, 9]为例)
-
初始数组:
[4, 6, 8, 5, 9] -
构建初始大顶堆:
-
最后一个非叶子节点索引:
(5//2)-1=1(节点值 6); -
对节点 1 堆化:无变化;
-
对节点 0(值 4)堆化:4 < 9(右子节点),交换后数组变为
[9, 6, 8, 5, 4],大顶堆构建完成;
-
-
提取堆顶(9):交换 0 和 4 索引 →
[4, 6, 8, 5, 9](有序区[9]),堆大小 = 4; -
对根节点 4 堆化 → 重构大顶堆
[8, 6, 4, 5](数组整体[8, 6, 4, 5, 9]); -
提取堆顶(8):交换 0 和 3 索引 →
[5, 6, 4, 8, 9](有序区[8,9]),堆大小 = 3; -
对根节点 5 堆化 → 重构大顶堆
[6, 5, 4](数组整体[6, 5, 4, 8, 9]); -
提取堆顶(6):交换 0 和 2 索引 →
[4, 5, 6, 8, 9](有序区[6,8,9]),堆大小 = 2; -
对根节点 4 堆化 → 重构大顶堆
[5,4](数组整体[5,4,6,8,9]); -
提取堆顶(5):交换 0 和 1 索引 →
[4,5,6,8,9](有序区[5,6,8,9]),堆大小 = 1; -
排序完成:
[4,5,6,8,9]。
完整实现(C 语言)
1.堆化函数
cpp
void Heap_Adjust(int arr[], int start, int end) {
while (1) {
int lastindex = start;
int left = 2 * start + 1;
int right = 2 * start + 2;
if (left <= end && arr[left] > arr[lastindex]) {
lastindex = left;
}
if (right <= end && arr[right] > arr[lastindex]) {
lastindex = right;
}
if (lastindex == start)break;
int tmp = arr[start];
arr[start] = arr[lastindex];
arr[lastindex] = tmp;
start = lastindex;
}
}
2.构建初始堆
cpp
void Heap_Sort(int arr[], int len) {
for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
Heap_Adjust(arr, i, len - 1);
}
}
3.排序循环
cpp
for (int i = 0; i < len - 1; i++) {
int tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
Heap_Adjust2(arr, 0, len - 2 - i);
}
时间复杂度与空间复杂度
1. 时间复杂度
-
最好 / 最坏 / 平均时间复杂度 :
O(n log n)(稳定无波动);-
构建初始堆:
O(n); -
排序循环:
n-1次迭代,每次堆化O(log n),总时间O(n log n);
-
-
稳定性:不稳定排序(交换堆顶和堆尾时,可能改变相等元素的相对顺序)。
2. 空间复杂度
-
递归实现 :
O(log n)(递归调用栈深度为堆的高度log n); -
非递归实现 :
O(1)(原地排序,仅需常数级临时空间);堆排序是 原地排序 (不占用额外内存,或仅占用常数内存),空间效率优于归并排序(O(n))。
适用场景与注意事项
1. 适用场景
-
大数据量排序 (如百万级、千万级数据):时间复杂度稳定
O(n log n),优于冒泡、插入排序; -
内存受限场景 :原地排序(非递归版
O(1)空间),无需额外内存,适合嵌入式系统、内存紧张的服务器; -
Top-K 问题 :无需全排序,构建大小为 K 的小顶堆,遍历剩余元素,仅保留比堆顶大的元素,最终堆内元素即为 Top-K(时间复杂度
O(n log K),比全排序高效)。
2. 注意事项
-
稳定性:堆排序不稳定,若需保持相等元素的相对顺序(如多字段排序),需选择归并排序或冒泡排序;
-
小规模数据效率 :小规模数据(
n < 100)时,堆排序效率低于插入排序、快速排序,需结合混合排序优化; -
栈溢出风险:递归实现适合中等数据量,大数据量建议用非递归堆化。
堆排序的核心是 "堆的构建与堆化 ",凭借 时间复杂度稳定 O (n log n)、原地排序 的特性,成为大数据量排序和 Top-K 问题的首选算法。其核心优势是 "无需额外内存、最坏情况性能有保障",核心劣势是 "不稳定、缓存命中率低、小规模数据效率一般"。