S16 排序算法--堆排序

堆排序是一种基于 "堆数据结构" 的排序算法,核心逻辑是 "利用堆的特性(父节点优先级高于子节点)筛选出最大值 / 最小值,逐步构建有序序列"。

堆的定义与特性

1. 堆的分类

  • 大顶堆(Max Heap):每个父节点的值 ≥ 其左右子节点的值(根节点是最大值);

  • 小顶堆(Min Heap) :每个父节点的值 ≤ 其左右子节点的值(根节点是最小值);堆排序默认用 大顶堆(升序排序),小顶堆可用于降序排序。

2.堆的存储方式(数组映射)

堆通常用 数组 存储(完全二叉树的特性适配数组索引),无需额外存储指针,空间效率高。假设数组索引从 0 开始,对于任意节点i

  • 左子节点索引:2i + 1

  • 右子节点索引:2i + 2

  • 父节点索引:(i - 1) // 2(整数除法)

3.堆化(Heapify):修复堆结构

功能:当某个节点破坏堆序性(如父节点 <子节点)时,将其 "下沉" 到正确位置,确保子树满足堆特性

输入:数组、当前节点索引、堆的大小(避免越界);

步骤(大顶堆为例):

  1. 假设当前节点i是最大值(初始候选);
  2. 找到i的左、右子节点,比较三者大小,更新最大值索引max_idx
  3. max_idx != i(说明子节点更大,破坏堆序):
  4. 交换arr[i]arr[max_idx]
  5. 递归对max_idx位置的节点堆化(交换后该子节点可能破坏子树堆结构);

时间复杂度:O(log n)(堆的高度为log n,节点下沉最多需log n次比较)。

构建初始堆

功能:将无序数组转化为大顶堆(或小顶堆);

核心逻辑:从 最后一个非叶子节点 开始,向前依次对每个节点执行堆化操作;

最后一个非叶子节点索引:(n // 2) - 1n是数组长度,叶子节点无需堆化);

时间复杂度:O(n)(看似n log n,实际多数节点深度小,数学推导后为O(n))。

堆排序的完整流程(升序排序)

堆排序的核心思想是 "反复提取堆顶最大值,构建有序序列",步骤如下:

  1. 构建初始大顶堆:将无序数组转化为大顶堆(根节点是最大值);

  2. 提取堆顶元素 :交换根节点(索引 0)和堆的最后一个元素(索引n-1),此时最大值被放到数组末尾(有序区);

  3. 缩小堆范围:堆的大小减 1(有序区不再参与堆操作);

  4. 堆化修复:对新的根节点执行堆化操作,重新构建大顶堆;

  5. 重复步骤 2-4:直到堆的大小为 1,数组完全有序。

流程可视化(以数组[4, 6, 8, 5, 9]为例)

  1. 初始数组:[4, 6, 8, 5, 9]

  2. 构建初始大顶堆:

    • 最后一个非叶子节点索引:(5//2)-1=1(节点值 6);

    • 对节点 1 堆化:无变化;

    • 对节点 0(值 4)堆化:4 < 9(右子节点),交换后数组变为[9, 6, 8, 5, 4],大顶堆构建完成;

  3. 提取堆顶(9):交换 0 和 4 索引 → [4, 6, 8, 5, 9](有序区[9]),堆大小 = 4;

  4. 对根节点 4 堆化 → 重构大顶堆[8, 6, 4, 5](数组整体[8, 6, 4, 5, 9]);

  5. 提取堆顶(8):交换 0 和 3 索引 → [5, 6, 4, 8, 9](有序区[8,9]),堆大小 = 3;

  6. 对根节点 5 堆化 → 重构大顶堆[6, 5, 4](数组整体[6, 5, 4, 8, 9]);

  7. 提取堆顶(6):交换 0 和 2 索引 → [4, 5, 6, 8, 9](有序区[6,8,9]),堆大小 = 2;

  8. 对根节点 4 堆化 → 重构大顶堆[5,4](数组整体[5,4,6,8,9]);

  9. 提取堆顶(5):交换 0 和 1 索引 → [4,5,6,8,9](有序区[5,6,8,9]),堆大小 = 1;

  10. 排序完成:[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 问题的首选算法。其核心优势是 "无需额外内存、最坏情况性能有保障",核心劣势是 "不稳定、缓存命中率低、小规模数据效率一般"。

相关推荐
烛衔溟2 小时前
C语言算法:排序算法入门
c语言·算法·排序算法·插入排序·冒泡排序·选择排序·多关键字排序
一匹电信狗2 小时前
【C++】封装红黑树实现map和set容器(详解)
服务器·c++·算法·leetcode·小程序·stl·visual studio
Laity______2 小时前
指针(2)
c语言·开发语言·数据结构·算法
是苏浙2 小时前
零基础入门C语言之C语言实现数据结构之顺序表经典算法
c语言·开发语言·数据结构·算法
CoovallyAIHub5 小时前
空间智能!李飞飞、LeCun&谢赛宁联手提出“空间超感知”,长文阐述世界模型蓝图
深度学习·算法·计算机视觉
Dave.B5 小时前
【VTK核心过滤器详解】:vtkCleanPolyData 多边形数据清洗实战指南
算法·vtk
AiXed5 小时前
PC微信 device uuid 算法
前端·算法·微信
@木辛梓6 小时前
指针,数组,变量
开发语言·c++·算法
苏纪云6 小时前
数据结构期中复习
数据结构·算法