S16 排序算法--堆排序
堆排序是一种基于 “堆数据结构” 的排序算法,核心逻辑是 “利用堆的特性(父节点优先级高于子节点)筛选出最大值 / 最小值,逐步构建有序序列”。
堆的定义与特性
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.堆化函数
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.构建初始堆
void Heap_Sort(int arr[], int len) {for (int i = (len - 1 - 1) / 2; i >= 0; i--) {Heap_Adjust(arr, i, len - 1);}
}3.排序循环
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 问题的首选算法。其核心优势是 “无需额外内存、最坏情况性能有保障”,核心劣势是 “不稳定、缓存命中率低、小规模数据效率一般”。
