当前位置: 首页 > news >正文

《堆的详解:结构、操作及堆排序算法》

目录

一.堆的概念与结构

1.1 堆的概念

1.2 堆性质: 

1.3 堆的结构定义

 二.堆的初始化和销毁

2.1 堆的初始化:

2.2 堆的销毁:

三.堆的插入数据(含向上调整算法的实现) 

3.1 插入逻辑

3.2 插入函数

3.3 向上调整算法

三. 堆的删除数据(含向下调整算法)

3.1 删除逻辑

3.2 向下调整算法

3.3 删除栈顶

四.堆的取堆顶 

五.堆排序的实现

5.1 借助数据结构实现排序

5.2 真正的堆排序算法


一.堆的概念与结构

堆(Heap)是一种特殊的完全二叉树,同时也是一种高效的数据结构,主要用于实现优先队列等场景。其核心特性是节点之间的数值关系满足严格的堆序性,具体可分为大堆(大顶堆) 和小堆(小顶堆)

1.1 堆的概念

堆的本质
堆是一棵完全二叉树(结构特性),且每个节点的值必须满足:

  • 若为大堆:每个节点的值大于等于其左右子节点的值(根节点是最大值)。
  • 若为小堆:每个节点的值小于等于其左右子节点的值(根节点是最小值)。

1.2 堆性质: 

  • 堆中某个结点的值总是不大于或者不小于其父结点的值
  • 堆总是一颗完全二叉树
  • 堆顶是最值(最大值或最小值)

1.3 堆的结构定义

// 堆的结构体定义(大堆)
typedef int HPDataType;  // 堆中元素的类型
typedef struct Heap {HPDataType* arr;       // 存储堆元素的数组int size;            // 当前堆中元素的个数int capacity;        // 堆的最大容量
} Heap;

 二.堆的初始化和销毁

2.1 堆的初始化:

// 初始化空堆
void HeapInit(Heap* hp) {if (hp == NULL) {return;  // 处理空指针}hp->arr = NULL;hp->size = 0;hp->capacity = 0;
}

2.2 堆的销毁:

销毁之前先检查数组为不为空,不为空就释放掉然后置空,并把size和capacity赋值为0

//销毁
void HPDestory(HP* php)
{assert(php);if (php->arr)free(php->arr);php->arr = NULL;php->size = php->capacity = 0;
}

三.堆的插入数据(含向上调整算法的实现) 

首先我们需要知道插入数据的逻辑 然后通过向上调整的方法来实现

向上调整算法

向上调整(也称 “上浮”)的核心是:新插入的元素从底部逐步向上移动,与父节点比较,若不满足堆序则交换,直到找到合适位置或到达根节点。

3.1 插入逻辑

  1. 空间检查:若堆已满,需扩容(类似动态数组扩容)。
  2. 添加元素:将新元素插入到堆的末尾(数组的最后一个位置)。
  3. 向上调整:从新元素位置开始,与父节点比较并交换,直至满足堆序性(大堆:子节点≤父节点;小堆:子节点≥父节点)。

如下图:

3.2 插入函数

首先插入时 我们需要判断空间是否足够 若不够的话就需要进行增容 代码如下

//往堆里面插入数据
void HPPush(HP* php, HPDataType x)
{//检查空间是否足够//不够就扩容if (php->size == php->capacity){int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->arr,newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}php->arr = tmp;php->capacity = newcapacity;}//够就直接插入php->arr[php->size++] = x;//向上调整AdjustUp(php->arr, php->size - 1);//因为前面size++了,所有传的是size-1
}

3.3 向上调整算法

//交换
void swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}
//向上调整
void AdjustUp(HPDataType*arr, int child)
{assert(arr);int parent = (child - 1) / 2;while (child > 0){//大堆:>//小堆:<if (arr[child] > arr[parent]){swap(&arr[child], &arr[parent]);child = parent;parent= (child - 1) / 2;}else{break;}}
}

大堆为例,步骤如下:

  • 设新元素索引为child,其父节点索引为parent = (child - 1) / 2(数组从 0 开始)。
  • a[child] > a[parent](大堆条件),交换两者,更新child = parent,继续向上比较。
  • 重复上述步骤,直到child = 0(根节点)或a[child] ≤ a[parent]

三. 堆的删除数据(含向下调整算法)

堆的删除操作通常指删除堆顶元素(根节点,即最大值或最小值),并通过向下调整算法重新维护堆的特性。以下是具体实现:

3.1 删除逻辑

  1. 替换堆顶:用堆的最后一个元素覆盖堆顶元素(避免直接删除堆顶导致结构破坏)。
  2. 缩减规模:堆的元素数量减 1(逻辑上移除最后一个元素)。
  3. 向下调整:从新的堆顶开始,与左右子节点中符合堆序的节点交换,直至满足堆的特性。

如下图

3.2 向下调整算法

向下调整(也称 “下沉”)的核心是:将替换后的堆顶元素逐步向下移动,与左右子节点中更符合堆序的节点(大堆选较大者,小堆选较小者)交换,直到找到合适位置或成为叶子节点。

大堆为例,步骤如下:

  • 设当前节点索引为parent,左子节点索引为child = 2 * parent + 1(数组从 0 开始)。
  • 若右子节点存在且大于左子节点,更新child为右子节点索引。
  • a[parent] < a[child](大堆条件),交换两者,更新parent = child,继续向下比较。
  • 重复上述步骤,直到child超出堆的范围或a[parent] ≥ a[child]
//向下调整
void AdjustDown(HPDataType* arr, int parent, int n)
{assert(arr);int child = 2 * parent + 1;while (child < n){//child+1也小于n//后面的小堆就是取小的,大堆取大的孩子if (child + 1 < n && arr[child] < arr[child + 1]){child++;}//大堆:>   小堆:<if (arr[child] > arr[parent]){swap(&arr[child], &arr[parent]);parent = child;child = 2 * parent + 1;}else{break;}}
}

3.3 删除栈顶

首先我们需要一个判断是否为空的函数

//判空
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}

删除栈顶函数

//删除(堆顶操作)
void HPPop(HP* php)
{assert(!HPEmpty(php));//首尾交换swap(&php->arr[0], &php->arr[php->size - 1]);php->size--;//向下调整AdjustDown(php->arr, 0, php->size);
}

先判断不为空,然后交换首尾,直接--size删掉,再通过向下调整算法把删除一个数据后的堆重新调整成大堆。 

四.堆的取堆顶 

首先判断是否为空 若不为空则直接返回栈顶元素arr[0]

//取堆顶
HPDataType HPTop(HP* php)
{assert(!HPEmpty(php));return php->arr[0];
}

五.堆排序的实现

这里向大家介绍两种方式 但其实第一种并不是真正意义上的排序 只是达到了排序的目的而已

下面介绍第一种

5.1 借助数据结构实现排序

#include"Heap.h"//堆排序--这不是真正的堆排序,而是利用了堆这个数据结构来实现的排序
void HeapSort1(int* arr, int n)
{HP sp;               // 定义一个堆结构体变量HPInit(&sp);         // 初始化堆(创建空堆)// 1. 将数组所有元素插入堆中for (int i = 0; i < n; i++){HPPush(&sp, arr[i]);  // 插入元素,内部会通过向上调整维护堆序}// 2. 从堆中提取元素,重构数组int i = 0;while (!HPEmpty(&sp))    // 当堆不为空时{HPDataType top = HPTop(&sp);  // 获取堆顶元素(最小值或最大值)arr[i++] = top;               // 将堆顶元素存入原数组,依次填充HPPop(&sp);                   // 删除堆顶元素,内部通过向下调整维护堆序}
}
int main()
{int arr[6] = { 30,56,25,15,70,10 };printf("排序之前:\n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);}printf("\n");HeapSort1(arr, 6);printf("排序之后:\n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

核心思路

  1. 借助堆存储元素:将待排序数组的所有元素依次插入堆中(构建堆)。
  2. 提取最值并重构数组:反复从堆顶提取最小值(或最大值),按顺序存入原数组,最终得到有序数组。

为什么说 “这不是真正的堆排序”?

真正的堆排序(如之前实现的HeapSort)是原地排序,直接在原数组上通过构建堆和调整堆实现排序,不需要额外的堆结构,空间复杂度为O(1)

而这段代码的本质是:

  1. 额外创建了一个堆(HP sp),需要O(n)的额外空间存储元素;
  2. 排序过程依赖于堆的插入和删除操作,本质是 “利用堆的特性进行排序”,而非严格意义上的原地堆排序。

5.2 真正的堆排序算法

通过构建大堆并反复提取最大值来实现数组的升序排序。其核心特点是原地排序(无需额外空间存储堆),充分利用堆的特性高效完成排序。以下是详细解释:

一、核心思路

  1. 构建大堆:将待排序数组转换为大堆(每个父节点的值 ≥ 子节点的值),此时堆顶(数组首位)是最大值。

  2. 排序过程

    • 交换堆顶(最大值)与当前堆的最后一个元素,将最大值放到数组末尾(最终位置)。

    • 缩小堆的范围(排除已排好的末尾元素),对新堆顶执行向下调整,重新维护大堆特性。

    • 重复上述步骤,直到所有元素排序完成。

二、代码逐段解析

1. HeapSort 函数(排序核心)

void HeapSort(int* arr, int n)
{// 1. 构建大堆for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, i, n);  // 从最后一个非叶子节点开始向下调整}// 2. 执行排序int end = n - 1;  // 标记当前堆的最后一个元素索引while (end > 0){// 交换堆顶(最大值)与当前堆的最后一个元素swap(&arr[0], &arr[end]);// 缩小堆范围(end减1),对新堆顶执行向下调整,重建大堆AdjustDown(arr, 0, end);end--;  // 下一次排序的堆范围更小}
}

(1)构建大堆

  • 关键代码for (int i = (n - 1 - 1) / 2; i >= 0; i--)

  • 计算最后一个非叶子节点的索引(n-1-1)/2(等价于 (n-2)/2)。

  • 数组从 0 开始,最后一个元素索引为 n-1,其父亲节点索引为 (n-1-1)/2
  • 从下往上调整:从最后一个非叶子节点开始,依次向上对每个节点执行 AdjustDown(向下调整),最终将整个数组转换为大堆。

  • 为什么从非叶子节点开始?
    叶子节点没有子节点,无需调整;非叶子节点可能破坏堆序,需通过向下调整使其子树满足大堆特性。

(2)排序过程

  • 交换堆顶与末尾元素swap(&arr[0], &arr[end])
    大堆的堆顶(arr[0])是当前堆中的最大值,交换后最大值被放到数组末尾(arr[end]),即它的最终位置。

  • 向下调整重建大堆AdjustDown(arr, 0, end)
    交换后堆顶元素可能破坏堆序,需从堆顶开始向下调整,但此时堆的范围已缩小为 [0, end-1]end 位置已排好序)。

  • 循环缩小范围end--
    每次循环后,已排序的元素增加一个,堆的范围持续缩小,直到 end=0(所有元素排序完成)。

2. main 函数(测试逻辑)

int main()
{int arr[6] = { 30,56,25,15,70,10 };  // 待排序数组printf("排序之前:\n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);  // 输出:30 56 25 15 70 10}printf("\n");HeapSort(arr, 6);  // 调用堆排序函数printf("排序之后:\n");for (int i = 0; i < 6; i++){printf("%d ", arr[i]);  // 输出:10 15 25 30 56 70(升序)}printf("\n");return 0;
}
  • 测试数组初始为 [30,56,25,15,70,10],经过堆排序后变为升序数组 [10,15,25,30,56,70]

三、关键辅助函数说明

  1. AdjustDown(向下调整算法)
    作用:当某个节点破坏大堆特性时,通过与左右子节点中较大的一个交换,逐步向下移动,直至子树重新满足大堆特性。
    (代码中未显示实现,但核心逻辑是:选择左右子节点的最大值,与父节点比较,不满足大堆则交换并继续调整。)

  2. swap(交换函数)
    作用:交换两个元素的值,用于将堆顶最大值移动到数组末尾。

四、为什么大堆能实现升序排序?

  • 大堆的堆顶始终是当前堆中的最大值,每次将最大值放到数组末尾,相当于 “从后往前” 依次确定最大元素的位置。

  • 经过 n-1 次交换和调整后,数组从前往后依次递增,最终实现升序。

五、算法特性

  • 时间复杂度O(n+logn)(构建堆 O(n) + 排序阶段 O(nlog+n))。

  • 空间复杂度O(1)(原地排序,仅需常数级额外空间)。

  • 不稳定性:交换过程可能改变相等元素的相对顺序(例如 [2, 2, 1] 排序后可能变为 [1, 2, 2],但两个 2 的原始顺序可能改变)。

总结

这段代码是标准的堆排序实现,通过 “构建大堆→交换堆顶与末尾→调整堆” 的循环,高效完成升序排序。其原地排序的特性和 O(n+log n) 的时间复杂度,使其在大规模数据排序中表现优异。


文章转载自:

http://fvDc8eHJ.Lsgsn.cn
http://ybMIaVWF.Lsgsn.cn
http://iVtLr1fn.Lsgsn.cn
http://cvpxwVVK.Lsgsn.cn
http://Tit8eQH7.Lsgsn.cn
http://MBS70jqM.Lsgsn.cn
http://o7IHIJDL.Lsgsn.cn
http://hTb1Eemu.Lsgsn.cn
http://ynq6uSfD.Lsgsn.cn
http://TQLka29K.Lsgsn.cn
http://pxaBTTuP.Lsgsn.cn
http://78mMMcn6.Lsgsn.cn
http://YCc4Bhp9.Lsgsn.cn
http://UynLgfEM.Lsgsn.cn
http://eEkJBJYa.Lsgsn.cn
http://huBOQqS2.Lsgsn.cn
http://u4h3azyL.Lsgsn.cn
http://fuP7W7mx.Lsgsn.cn
http://UjfuCEMr.Lsgsn.cn
http://sKBZnVvw.Lsgsn.cn
http://7ckOTxgb.Lsgsn.cn
http://t0C3tydh.Lsgsn.cn
http://C3R0egRP.Lsgsn.cn
http://JBtVNFMP.Lsgsn.cn
http://lGL73agj.Lsgsn.cn
http://8iz8QnH1.Lsgsn.cn
http://tnxeNO19.Lsgsn.cn
http://Au90dS0I.Lsgsn.cn
http://7Z9l1Gtx.Lsgsn.cn
http://3P7ffiw1.Lsgsn.cn
http://www.dtcms.com/a/380572.html

相关文章:

  • iOS 能耗监控与电池优化实战:如何查看App耗电量、分析CPU、GPU内存使用、(uni-app iOS开发性能调试指南)
  • MSS 到底是什么?Wireshark 分析TCP过程
  • CTFHub SSRF通关笔记8:数字IP Bypass 原理详解与渗透实战
  • 从西贝“预制菜”事件看明厨亮灶视觉算法价值
  • 【0435】heap toast 内核实现之 计算 toast tuple header 开销
  • 发现渐开线蜗杆成型铣刀齿形可以这样计算
  • Krisp-世界排名第一的AI噪音消除应用
  • 【116】基于51单片机GSM智能快递柜【Proteus仿真+Keil程序+报告+原理图】
  • 测试开发实战:接口自动化框架设计与性能瓶颈排
  • Linux -- 信号【上】
  • 敏捷适合短期项目还是长期项目
  • Android Sip电话(PJSP)
  • C语言——操作符(逻辑、条件、关系)
  • OpenHarmony:App(页面跳转)
  • 什么是快照式光谱成像相机?
  • 如何通过ESB整合美妆零售全渠道订单流、物流与金流?
  • java整合阿里云短信服务实现验证码功能
  • Excel转图片excel2img库bug修复:AttributeError ‘parent‘ 问题解决方案
  • 【秋招笔试】2025.09.10华为ai算法岗笔试真题
  • 【深度学习-Day 47】告别单向依赖:深入解析双向RNN与堆叠RNN,解锁序列建模新高度
  • 为Excel和WPS表格多个不连续单元格设置同样的批注
  • 【Vite】打包优化
  • 3 遥感与机器学习
  • 汽车功能安全 Functional Safety ISO 26262 测试之一
  • 第八章 惊喜08 减负
  • Redis 持久化详解:RDB 与 AOF 原理、配置及选型
  • MySQL 索引学习全景笔记
  • Ethernaut Foundry Solutions - 完整系列教程
  • 【ARDUINIO】从串口读取数据的方法总结
  • Redis环境搭建指南:Windows/Linux/Docker多场景安装与配置