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

【附C源码】从零实现C语言堆数据结构:原理、实现与应用

【附C源码】从零实现C语言堆数据结构:原理、实现与应用

堆(Heap)作为优先级队列的经典实现,在任务调度、图算法(如Dijkstra、Prim)、Top-K问题等场景中有着广泛应用。本文将深入探讨堆的核心原理,并基于C语言标准库实现一个完整的、可用于生产环境的最小堆与最大堆。

一、堆的本质与性质

堆是一种特殊的完全二叉树,其核心特性在于父节点与子节点之间的序关系:

  • 最小堆:任意节点的值不大于其子节点的值,堆顶为全局最小值
  • 最大堆:任意节点的值不小于其子节点的值,堆顶为全局最大值

由于完全二叉树的结构特性,堆可以高效地映射到数组中存储,避免了指针带来的额外内存开销。

数组映射关系

对于索引为iii的节点:

  • 父节点索引:parent(i)=⌊(i−1)/2⌋parent(i) = \lfloor (i-1)/2 \rfloorparent(i)=⌊(i1)/2
  • 左子节点索引:left(i)=2i+1left(i) = 2i + 1left(i)=2i+1
  • 右子节点索引:right(i)=2i+2right(i) = 2i + 2right(i)=2i+2

数组存储

索引: 0, 1, 2, 3, 4, 5, 6

值: 10, 20, 15, 30, 25, 18, 22

堆的树形结构

10

20

15

30

25

18

22

二、核心操作的时间复杂度分析

操作时间复杂度说明
插入元素O(log⁡n)O(\log n)O(logn)从叶节点向上调整
删除堆顶O(log⁡n)O(\log n)O(logn)将尾元素移至堆顶后向下调整
查看堆顶O(1)O(1)O(1)直接访问数组首元素
Floyd建堆O(n)O(n)O(n)自底向上批量建堆,优于逐元素插入

Floyd建堆的线性复杂度是一个常被忽视但极为重要的优化点。对于nnn个元素的建堆过程,时间复杂度可推导为:

T(n)=∑h=0⌊log⁡n⌋⌈n2h+1⌉⋅O(h)=O(n) T(n) = \sum_{h=0}^{\lfloor \log n \rfloor} \lceil \frac{n}{2^{h+1}} \rceil \cdot O(h) = O(n)T(n)=h=0logn2h+1nO(h)=O(n)

三、数据结构定义

typedefenum{MIN_HEAP,// 父节点 <= 子节点MAX_HEAP// 父节点 >= 子节点}HeapType;typedefstruct{int*data;// 动态数组intsize;// 当前元素数intcapacity;// 数组容量HeapType type;// 堆类型标识}Heap;

采用函数指针或宏定义实现比较逻辑的抽象,可以在编译期确定堆类型,避免运行时的分支判断开销。

四、关键算法实现

4.1 向上调整(Heapify Up)

插入操作将新元素置于数组末尾,随后沿父节点方向逐级比较、交换,直至满足堆性质。

新元素插入末尾

当前节点优先级高于父节点?

交换两节点

索引移至父节点

调整完成

staticvoidheapifyUp(Heap*h,intindex){while(index>0){intparent=PARENT(index);if(hasHigherPriority(h,h->data[index],h->data[parent])){SWAP(h->data[index],h->data[parent]);index=parent;}else{break;}}}

4.2 向下调整(Heapify Down)

删除堆顶时,将末尾元素移至堆顶后,从根节点开始与子节点比较,与优先级较高的子节点交换,直至叶子节点。

staticvoidheapifyDown(Heap*h,intindex){while(true){intleft=LEFT(index);intright=RIGHT(index);intpriority=index;if(left<h->size&&hasHigherPriority(h,h->data[left],h->data[priority]))priority=left;if(right<h->size&&hasHigherPriority(h,h->data[right],h->data[priority]))priority=right;if(priority==index)break;SWAP(h->data[index],h->data[priority]);index=priority;}}

4.3 Floyd建堆算法

与逐个插入的O(nlog⁡n)O(n \log n)O(nlogn)复杂度相比,Floyd算法通过自底向上的批量调整,将建堆复杂度优化至线性。

Heap*heapBuild(int*arr,intn,HeapType type){Heap*h=heapCreate(n>4?n:4,type);memcpy(h->data,arr,sizeof(int)*n);h->size=n;// 从最后一个非叶子节点开始向下调整for(inti=PARENT(n-1);i>=0;i--){heapifyDown(h,i);}returnh;}

五、堆排序的实现

堆排序是一种原地、不稳定排序算法,核心思想是:

  1. 将待排序数组构建为最大堆(升序)或最小堆(降序)
  2. 反复将堆顶元素与末尾元素交换,堆规模减一
  3. 对新的堆顶执行向下调整
  4. 重复步骤2-3直至堆为空
voidheapSort(int*arr,intn,bool ascending){HeapType type=ascending?MAX_HEAP:MIN_HEAP;Heap*h=heapBuild(arr,n,type);for(inti=n-1;i>=0;i--){heapPop(h,&arr[i]);}heapDestroy(h);}

堆排序的时间复杂度恒为O(nlog⁡n)O(n \log n)O(nlogn),空间复杂度为O(1)O(1)O(1)(若采用原地建堆优化)。

六、工程实践要点

6.1 动态扩容策略

当容量不足时,采用倍增策略扩容,均摊插入复杂度仍为O(1)O(1)O(1)

staticboolheapExpand(Heap*h){intnewCapacity=h->capacity*2;int*newData=realloc(h->data,sizeof(int)*newCapacity);if(!newData)returnfalse;h->data=newData;h->capacity=newCapacity;returntrue;}

6.2 删除任意位置元素

删除非堆顶元素时,用末尾元素填充被删位置,随后同时执行向上和向下调整,确保堆性质恢复。

boolheapRemoveAt(Heap*h,intindex,int*outValue){*outValue=h->data[index];h->data[index]=h->data[--h->size];// 双方向调整,仅需其中一个会生效heapifyUp(h,index);heapifyDown(h,index);returntrue;}

七、应用场景与扩展

7.1 优先级队列

堆是实现优先级队列的理想数据结构,支持高效地获取最高优先级元素。

7.2 Top-K问题

维护一个大小为K的最小堆,遍历数据流时,当新元素大于堆顶则替换,最终堆中即为最大的K个元素。

7.3 图算法优化

Dijkstra最短路径算法和Prim最小生成树算法中,使用堆优化可将时间复杂度从O(V2)O(V^2)O(V2)降至O((V+E)log⁡V)O((V+E)\log V)O((V+E)logV)

八、总结

本文实现的堆数据结构具备以下特点:

  • 支持最小堆与最大堆双模式
  • 采用动态数组实现,自动扩容
  • Floyd建堆算法保证线性建堆复杂度
  • 完整的堆排序功能
  • 零第三方依赖,仅使用C标准库

完整代码已开源,可作为学习数据结构的参考实现,也可根据实际需求扩展为泛型版本(通过void*与比较函数指针)。


⚠️源码地址:https://github.com/anjuxi/C-heap

http://www.rkmt.cn/news/1293726.html

相关文章:

  • 如何轻松实现专业级音频处理:5个AI场景完全指南
  • STM32CubeMX实战:5分钟搞定MAX31865 PT100测温,从SPI配置到温度读取全流程
  • 3分钟搞定容器镜像加速:public-image-mirror 终极实战指南
  • 汉森软件冲刺港股:年营收6亿 净利1.4亿 已获IPO备案
  • 深度解析Gopeed下载架构:从HTTP 403错误处理到性能优化的完整实践
  • Taskbar Groups:Windows任务栏分组的终极解决方案
  • 不止于定位:用C++解析NMEA-0813协议,挖掘GGA、GSA、GSV报文里的隐藏信息
  • OpenSSL 3.x 国密SM2/SM3实战:从密钥生成到数据验签的C++封装指南
  • 网易云音乐网页版功能扩展终极指南:如何深度定制你的音乐体验
  • MirrorCaster终极指南:5步解决Android投屏延迟卡顿问题
  • 保姆级教程:在Win10上用VS2022配置TensorRT 8.5.2.2,跑通第一个MNIST推理Demo
  • AI任务管理框架:从工作流引擎到智能体开发实践
  • 10分钟掌握终极笔记备份:evernote-backup工具完全指南
  • Qt环境变量实战:用qputenv与qgetenv构建动态配置的跨平台应用
  • 我扒了4款过知网AIGC检测降AI软件的退款门槛!哪款AI率超20%就能全额退
  • 性能实测:HC32F4A0的FPU加持下,CMSIS-DSP做1024点FFT到底有多快?
  • 如何在Mac上免费一键解锁CrossOver游戏兼容性:CXPatcher完全指南
  • 开源直播推流工具clawstage:模块化设计与核心实现解析
  • 告别Keil!用STM32CubeIDE给STM32F103C8T6做双路ADC采样,DMA+中断实战避坑
  • 别再到处找安装包了!Windows系统下FreeCAD 0.18.4保姆级安装与汉化教程
  • WIN11下NFS21闪退终结指南:从黑屏到流畅狂飙的实战修复
  • Golang怎么用Go实现数据导入导出平台_Golang如何支持CSV和Excel格式的批量数据导入导出【实战】
  • 基于MCP协议构建AI工具调用中枢:Skillsync-MCP架构解析与实践
  • 【ElevenLabs尼泊尔文语音实战指南】:20年AI语音工程师亲授7大避坑要点与本地化部署全流程
  • 如何快速优化EVE Online舰船配置:免费专业工具指南
  • 第四章:深入系统底层 —— Root提权与内核漏洞
  • MAA智能助手:突破性图像识别技术如何重新定义明日方舟游戏自动化
  • 告别臃肿!G-Helper:华硕笔记本轻量控制中心的终极指南
  • QT5之串口
  • 【Java用法】jar包运行后显示 没有主清单属性