> 技术文档 > 【数据结构】堆的完整实现_堆实现

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


堆的完整实现

  • 堆的完整实现
  • GitHub地址
  • 前言
  • 堆的核心功能实现
    • 重温堆的定义
    • 堆结构定义
    • 1. 堆初始化与销毁
    • 2. 元素交换函数
    • 3. 堆化操作
      • 向上调整(子→父)
      • 向下调整(父→子)
    • 4. 堆元素插入
    • 5. 堆元素删除
    • 6. 辅助功能函数
      • 堆的判空
      • 获取堆顶元素
      • 获取堆的大小
    • 所有代码实现
      • 头文件
      • 源文件
  • 结语

堆的完整实现

GitHub地址

有梦想的电信狗

前言

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


堆的核心功能实现

重温堆的定义

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

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

在这里插入图片描述

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

在这里插入图片描述

二叉树的顺序存储,是堆的前身

在顺序存储的二叉树上加一些限定条件,定义为堆

  • 小根堆 : 树中所有父结点都小于或等于子节点
  • 大根堆 树中所有父结点都大于或等于子节点
  • 堆可以用来排序,但堆并非是有序的!

堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

在这里插入图片描述

堆结构定义

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

结构说明

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

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

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

功能一览

//堆初始化与销毁void HeapInit(Heap* pheap);void HeapDestroy(Heap* pheap);//堆插入和删除void HeapPush(Heap* pheap, HeapDataType data);void HeapPop(Heap* pheap);//堆的向上 向下调整void AdjustUp(HeapDataType* arr, int child);void AdjustDown(HeapDataType* arr, int size, int parent);//交换堆中的元素void Swap(HeapDataType* left, HeapDataType* right);//获取堆顶元素HeapDataType HeapTop(Heap* pheap);//判断堆是否为空bool HeapEmpty(Heap* pheap);//获取堆的sizeint HeapSize(Heap* pheap);

1. 堆初始化与销毁

初始化

//堆初始化void HeapInit(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的倍数)

销毁

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

实现思路

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

2. 元素交换函数

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

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

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


3. 堆化操作

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

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

向上调整(子→父)

在这里插入图片描述

// 插入数据向上调整, 删除数据向下调整//向上调整 或向下调整的条件是,左右子树 必须是大堆 或者 小堆void AdjustUp(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插入的过程
// 向上调整建堆void creatHeapUp(HeapDataType* arr, int size) {for (int i = 1; i < size; ++i)AdjustUp(arr, i);}

向下调整(父→子)

在这里插入图片描述

// 向下调整 到叶子结点结束,叶子结点的左孩子 的下标 大于 size size是数组的大小void AdjustDown(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的时候左右子树都是大堆

// 向下调整建堆void creatHeapDown(HeapDataType* arr, int size) {for (int i = (size - 1 - 1) / 2; i >= 0; --i)AdjustDown(arr, size, i);}

小根堆的调整:可与大根堆类比
在这里插入图片描述


4. 堆元素插入

  • 向上调整的过程

在这里插入图片描述

// 插入,不能指定位置插入。// 因为新元素插入后要进行调整使其满足堆的结构,指定的位置不一定是最终调整后的位置void HeapPush(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. 堆的父子关系全乱了// 删堆顶的元素,将第一个元素和最后一个元素交换(最大限度的保持了原有的关系),再向下调整维持堆的大小关系void HeapPop(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号元素就是堆顶元素

获取堆的大小

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

功能说明

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

所有代码实现

头文件

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

源文件

//Heap.c#include \"Heap.h\"//堆初始化void HeapInit(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;}void HeapDestroy(Heap* pheap) {assert(pheap);free(pheap->base);pheap->base = NULL;pheap->capacity = pheap->size = 0;}void Swap(HeapDataType* child, HeapDataType* parent) {HeapDataType temp = *child;*child = *parent;*parent = temp;}// 插入,不能指定位置插入。// 因为新元素插入后要进行调整使其满足堆的结构,指定的位置不一定是最终调整后的位置void HeapPush(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. 堆的父子关系全乱了// 删堆顶的元素,将第一个元素和最后一个元素交换(最大限度的保持了原有的关系),再向下调整维持堆的大小关系void HeapPop(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);}}// 插入数据向上调整, 删除数据向下调整//向上调整 或向下调整的条件是,左右子树 必须是大堆 或者 小堆void AdjustUp(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是数组的大小void AdjustDown(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;}//获取堆的sizeint HeapSize(Heap* pheap) {assert(pheap);return pheap->size;}

结语

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

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