【数据结构】堆的完整实现

【数据结构】堆的完整实现

堆的完整实现

堆的完整实现

GitHub地址

有梦想的电信狗

前言

堆(Heap)是一种特殊的完全二叉树数据结构,常用于实现优先级队列。本文基于C语言实现大跟堆,包含核心操作:插入元素、删除堆顶元素、堆化操作等。以下是完整实现及详细解析。


堆的核心功能实现

重温堆的定义

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。

现实中我们通常把堆(一种特殊的二叉树)使用顺序结构的数组来存储。

在这里插入图片描述

需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

在这里插入图片描述
二叉树的顺序存储,是堆的前身

在顺序存储的二叉树上加一些限定条件,定义为堆
  • 小根堆 : 树中所有父结点都小于或等于子节点
  • 大根堆 树中所有父结点都大于或等于子节点
  • 堆可以用来排序,但堆并非是有序的!

堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。
在这里插入图片描述

堆结构定义

//实现大堆 这里以实现大根堆为例子typedefint HeapDataType;//定义堆中存放的数据类型,方便修改typedefstructHeapNode{ HeapDataType* base;// 堆存储数组基地址int size;// 当前元素个数int capacity;// 堆容量}Heap;

结构说明

  • base:动态数组基地址,用于存储堆元素,用数组来顺序存储
  • size:当前堆中元素数量
  • capacity:数组总容量

数组存储的二叉树的下标特性

  • parent = (child - 1) / 2
  • lefichild = parent * 2 + 1
  • lefichild = parent * 2 + 2

功能一览

//堆初始化与销毁voidHeapInit(Heap* pheap);voidHeapDestroy(Heap* pheap);//堆插入和删除voidHeapPush(Heap* pheap, HeapDataType data);voidHeapPop(Heap* pheap);//堆的向上 向下调整voidAdjustUp(HeapDataType* arr,int child);voidAdjustDown(HeapDataType* arr,int size,int parent);//交换堆中的元素voidSwap(HeapDataType* left, HeapDataType* right);//获取堆顶元素 HeapDataType HeapTop(Heap* pheap);//判断堆是否为空 bool HeapEmpty(Heap* pheap);//获取堆的sizeintHeapSize(Heap* pheap);

1. 堆初始化与销毁

初始化

//堆初始化voidHeapInit(Heap* pheap){assert(pheap); pheap->base =(HeapDataType*)malloc(sizeof(HeapDataType)*4);if(pheap->base ==NULL){perror("malloc failed\n");return;} pheap->size =0; pheap->capacity =4;}

实现思路

  • 断言指针有效性,堆结构指针必须存在
  • 为堆分配初始容量(暂设置为4个元素空间),申请空间失败时报错并返回
  • 初始化size0表示空堆,capacity初始化为申请的空间
  • 时间复杂度:O(1)

注意事项

  • 必须进行指针有效性断言检查
  • 初始容量不宜过小(建议为4的倍数)

销毁

//清理资源voidHeapDestroy(Heap* pheap){assert(pheap);free(pheap->base); pheap->base =NULL; pheap->capacity = pheap->size =0;}

实现思路

  • assert断言堆结构指针不为空
  • free释放动态分配的存储空间
  • 将指针置NULL防止野指针
  • sizecapacity都置为0
  • 时间复杂度:O(1)

2. 元素交换函数

voidSwap(HeapDataType* child, HeapDataType* parent){ HeapDataType temp =*child;*child =*parent;*parent = temp;}

功能:交换父子节点数值
使用场景:堆化(向上调整/向下调整)时的元素位置调整

交换函数功能较为简单,此处不过多赘述。

3. 堆化操作

向上调整 或向下调整的条件是,左右子树 必须是 大堆 或者 小堆

向上调整 或 向下调整都是为了,使原数组成为堆(大堆或小堆)

向上调整(子→父)

在这里插入图片描述
// 插入数据向上调整, 删除数据向下调整//向上调整 或向下调整的条件是,左右子树 必须是大堆 或者 小堆voidAdjustUp(HeapDataType* arr,int child){//child是需要调整的节点的下标assert(arr);int parent =(child -1)/2;while(child >0){// child 等于 0 或小于 0 时就不用再调整了if(arr[child]> arr[parent]){Swap(&arr[child],&arr[parent]); child = parent; parent =(child -1)/2;}//child <= parent 时elsebreak;}}// 循环部分// 循环条件 写成 while (parent >= 0) // 个人建议while的循环条件内不要写太复杂的条件//写成 child > 0 会更好 因为 最坏时 child 为 0 ,此时parent = (child-1)/2 也为0//因此 实际上 parent 不会为 <= 0

功能:将新插入堆的元素调整到合适位置 ,使其满足堆的性质

  • child是新插入元素的下标,一般是size-1(即通过尾插进入的最后一个元素的下标)
  • arr是待调整的数组指针,断言arr非空
  • child 为子节点,向上调整思路
    • 通过parent = (child - 1) / 2,计算待调整结点的父节点的下标
    • child下标为0时,代表最后一次交换(调整)已结束。因此循环结束条件为child == 0
    • 比较父子结点的大小,子节点大于父节点时,交换,满足大根堆的逻辑,同时下标childparent进行更新
    • 当前child结点<parent结点时,代表以满足大根堆,直接结束循环即可。
  • 时间复杂度为 O(logN)

终止条件

  • 以下条件满足其一,循环即可终止。
    • 子节点值 父节点值
    • 到达堆顶(child=0

总结

  • 向上调整是从最后一个元素(size - 1)开始调整,一般是新插入的元素
  • 因此利用向上调整进行建堆的过程,需要模拟元素一个个插入的过程
    • AdjustUp(arr, i)arr是需要调整的数组,i表示child的下标
    • 可以使i从1开始,逐1增长的过程,模拟了一个个child插入的过程
// 向上调整建堆voidcreatHeapUp(HeapDataType* arr,int size){for(int i =1; i < size;++i)AdjustUp(arr, i);}

向下调整(父→子)

在这里插入图片描述
// 向下调整 到叶子结点结束,叶子结点的左孩子 的下标 大于 size size是数组的大小voidAdjustDown(HeapDataType* arr,int size,int parent){assert(arr);assert(parent >=0&& parent < size);//parent非负 且 不能越界int child = parent *2+1;while(child < size){//检查 child+1 是否越界 以及 找出左右孩子中更大的那个//child + 1 >= size 时,表示当前父节点只有左孩子if(child +1< size && arr[child +1]> arr[child])++child;if(arr[child]> arr[parent]){Swap(&arr[parent],&arr[child]); parent = child; child = parent *2+1;}elsebreak;}}

实现思路

  • 断言arrparent >= 0 && parent < size,保证arr存在,parent非负且不越界
    • parent一般是0从堆顶元素开始调整,尽可能地保持了堆的结构。
  • child = parent * 2 + 1:计算左孩子的下标,右孩子的下标是左孩子下标+1
  • child >= size时,代表最后一个元素已完成交换,循环结束
  • 检查child+1是否越界以及 找出左右孩子中更大的那个,让较大者与parent结点进行比较。
  • child更大时,与parent结点进行交换,childparent接着移动。

实现要点

  1. 总是与较大的子节点比较
  2. 循环终止条件(满足其一即终止):
    • parent数据 ≥ 两个child节点数据,此时已符合大根堆的条件,向下调整完成
    • child>size时,所有的节点已完成交换,此时已符合大根堆的条件,向下调整完成

时间复杂度为 O(logN)

总结

  • 不能从 最顶端 0 开始向下调整建堆,选择从 ==最后一个叶子结点的父亲(最后一个非叶子结点)==开始调整
  • 向下/向上调整的条件是,左右子树都是大堆,从下开始往上调,可以保证调到0的时候左右子树都是大堆
// 向下调整建堆voidcreatHeapDown(HeapDataType* arr,int size){for(int i =(size -1-1)/2; i >=0;--i)AdjustDown(arr, size, i);}

小根堆的调整:可与大根堆类比

在这里插入图片描述

4. 堆元素插入

  • 向上调整的过程
在这里插入图片描述
// 插入,不能指定位置插入。// 因为新元素插入后要进行调整使其满足堆的结构,指定的位置不一定是最终调整后的位置voidHeapPush(Heap* pheap, HeapDataType data){assert(pheap);//空堆也可以push,但需保证结构体存在//插入检查是否需要扩容if(pheap->size == pheap->capacity){ HeapDataType* newSpace =(HeapDataType*)realloc(pheap->base,sizeof(HeapDataType)* pheap->capacity *2);if(newSpace ==NULL){perror("realloc failed\n");return;} pheap->base = newSpace; pheap->capacity *=2;}//更新 pheap->base[pheap->size]= data; pheap->size++;//插入后需向上调整,保证插入后满足堆的特性AdjustUp(pheap->base, pheap->size -1);//size++ 后,size-1 是新插入元素的下标}

实现思路

  1. 断言堆存在检查并扩容(2倍扩容策略)
    • 扩容:
      • 开空间,二倍扩容
      • 判断是否开辟成功
      • 更改指针和容积
  2. 将新元素插入数组末尾,更新size,尽可能的保持原来堆的结构。
  3. 执行向上调整操作维护堆结构

时间复杂度

  • 最优:O(logN)(不需要扩容)
  • 最差:O(N)(触发扩容)

5. 堆元素删除

  • 向下调整的过程
在这里插入图片描述
//堆的删除 应当删除堆顶的元素,删除堆尾的数据没有意义。//删除最大的或最小的,可以选出第二大或第二小的//挪动删除(直接删)的缺点: 1. 效率低下O(n) 2. 堆的父子关系全乱了// 删堆顶的元素,将第一个元素和最后一个元素交换(最大限度的保持了原有的关系),再向下调整维持堆的大小关系voidHeapPop(Heap* pheap){assert(pheap && pheap->size >0);assert(!HeapEmpty(pheap));//删除堆顶元素 交换堆顶元素 和 堆尾元素Swap(&pheap->base[0],&pheap->base[pheap->size -1]); pheap->size--;//删除数据,让size-1 size--之后,可能会为0// 仅当堆非空时进行向下调整if(pheap->size >0){AdjustDown(pheap->base, pheap->size,0);}}

实现思路

  • 断言堆存在并且确保堆为非空。
  • 通过交换堆顶元素和堆尾元素,并更改size--来实现数组内元素的删除
  • 通过size--的方式删除元素,向下调整时,要确保size的值不为0

注意事项

  • 删除前必须检查堆是否为空
  • size减至0时无需向下调整操作

6. 辅助功能函数

堆的判空

//判断堆是否为空 bool HeapEmpty(Heap* pheap){assert(pheap);return pheap->size ==0;}

获取堆顶元素

//获取堆顶元素 HeapDataType HeapTop(Heap* pheap){assert(pheap);assert(pheap->base);return pheap->base[0];}
  • 0号元素就是堆顶元素

获取堆的大小

//获取堆的sizeintHeapSize(Heap* pheap){assert(pheap);return pheap->size;}

功能说明

  • HeapTop:获取堆顶元素(极值)
    • 大根堆时是极大值
    • 小跟堆时是极小值
  • HeapEmpty:判断堆是否为空
  • HeapSize:获取当前元素个数

所有代码实现

头文件

// Heap.h#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdbool.h>//二叉树的顺序存储,是堆的前身//在顺序存储的二叉树上加一些限定条件,定义为堆////堆中某个节点的值总是不大于或不小于其父节点的值//堆总是一棵 完全二叉树////小根堆 树中所有父亲都小于或等于孩子//大根堆 树中所有父亲都大于或等于孩子//堆可以用来排序,但堆并非是有序的 ,建堆的过程实际就是排序的过程//堆的向上调整//实现大堆 这里以实现大根堆为例子typedefint HeapDataType;typedefstructHeapNode{ HeapDataType* base;int size;int capacity;}Heap;//交换堆中的元素voidSwap(HeapDataType* child, HeapDataType* parent);//堆的向上 向下调整voidAdjustUp(HeapDataType* arr,int child);voidAdjustDown(HeapDataType* arr,int size,int parent);//堆初始化与销毁voidHeapInit(Heap* pheap);voidHeapDestroy(Heap* pheap);//堆插入和删除voidHeapPush(Heap* pheap, HeapDataType data);voidHeapPop(Heap* pheap);//获取堆顶元素 HeapDataType HeapTop(Heap* pheap);//判断堆是否为空 bool HeapEmpty(Heap* pheap);//获取堆的sizeintHeapSize(Heap* pheap);

源文件

//Heap.c#include"Heap.h"//堆初始化voidHeapInit(Heap* pheap){assert(pheap); pheap->base =(HeapDataType*)malloc(sizeof(HeapDataType)*4);if(pheap->base ==NULL){perror("malloc failed\n");return;} pheap->size =0; pheap->capacity =4;}voidHeapDestroy(Heap* pheap){assert(pheap);free(pheap->base); pheap->base =NULL; pheap->capacity = pheap->size =0;}voidSwap(HeapDataType* child, HeapDataType* parent){ HeapDataType temp =*child;*child =*parent;*parent = temp;}// 插入,不能指定位置插入。// 因为新元素插入后要进行调整使其满足堆的结构,指定的位置不一定是最终调整后的位置voidHeapPush(Heap* pheap, HeapDataType data){assert(pheap);//空堆也可以push,但需保证结构体存在//插入检查是否需要扩容if(pheap->size == pheap->capacity){ HeapDataType* newSpace =(HeapDataType*)realloc(pheap->base,sizeof(HeapDataType)* pheap->capacity *2);if(newSpace ==NULL){perror("realloc failed\n");return;} pheap->base = newSpace; pheap->capacity *=2;} pheap->base[pheap->size]= data; pheap->size++;//插入后需向上调整,保证插入后满足堆的特性AdjustUp(pheap->base, pheap->size -1);//size++ 后,size-1 是新插入元素的下标}//堆的删除 要删除堆顶的数,删除堆尾的数据没有意义,删除最大的或最小的,可以选出第二大或第二小的//挪动删除(直接删)的缺点 1. 效率低下O(n) 2. 堆的父子关系全乱了// 删堆顶的元素,将第一个元素和最后一个元素交换(最大限度的保持了原有的关系),再向下调整维持堆的大小关系voidHeapPop(Heap* pheap){assert(pheap && pheap->size >0);assert(!HeapEmpty(pheap));//删除堆顶元素 交换堆顶元素 和 堆尾元素Swap(&pheap->base[0],&pheap->base[pheap->size -1]); pheap->size--;//删除数据,让size-1 size--之后,可能会为0// 仅当堆非空时进行向下调整if(pheap->size >0){AdjustDown(pheap->base, pheap->size,0);}}// 插入数据向上调整, 删除数据向下调整//向上调整 或向下调整的条件是,左右子树 必须是大堆 或者 小堆voidAdjustUp(HeapDataType* arr,int child){assert(arr);int parent =(child -1)/2;//while (parent >= 0) { // 个人建议while的循环条件内不要写太复杂的条件//写成 child > 0 会更好 因为 最坏时 child 为 0 ,此时parent = (child-1)/2 也为0//因此 实际上 parent 不会为 <= 0while(child >0){// child 等于 0 或小于 0 时就不用再调整了if(arr[child]> arr[parent]){// arr[child] > arr[parent] 建的是大堆Swap(&arr[child],&arr[parent]); child = parent; parent =(child -1)/2;}else//child <= parent 时break;}}//向下向上调整时间复杂度为 logN// 向下调整 到叶子结点结束,叶子结点的左孩子 的下标 大于 size size是数组的大小voidAdjustDown(HeapDataType* arr,int size,int parent){assert(arr);assert(parent >=0&& parent < size);//parent非负 且 不能越界int child = parent *2+1;while(child < size){//检查 child+1 是否越界 以及 找出左右孩子中更大的那个if(child +1< size && arr[child +1]> arr[child])++child;if(arr[child]> arr[parent]){Swap(&arr[parent],&arr[child]); parent = child; child = parent *2+1;}elsebreak;}}//获取堆顶元素 HeapDataType HeapTop(Heap* pheap){assert(pheap);assert(pheap->base);return pheap->base[0];}//判断堆是否为空 bool HeapEmpty(Heap* pheap){assert(pheap);return pheap->size ==0;}//获取堆的sizeintHeapSize(Heap* pheap){assert(pheap);return pheap->size;}

结语

本文完整实现了基于数组存储的大根堆结构,重点阐释了堆化过程中向上调整与向下调整的核心逻辑。通过动态数组管理、二倍扩容策略及父子节点下标计算,构建了插入元素时末尾上浮、删除堆顶时首尾交换后根节点下沉的高效操作,确保堆性质在O(logN)时间内得以维护。关键点在于理解完全二叉树顺序存储的特性,以及插入/删除时通过逐层比较交换维护父节点≥子节点的规则。实际应用中可调整比较逻辑切换大小堆,适用于优先队列、堆排序等场景,注意边界处理避免空堆删除和扩容失败问题。

分享到此结束啦
一键三连,好运连连!

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk