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

堆的完整实现

堆的完整实现

GitHub地址

有梦想的电信狗

前言

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


堆的核心功能实现

重温堆的定义

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

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

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

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

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

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

堆的性质

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

堆结构定义

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

功能一览

c 复制代码
//堆初始化与销毁
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);
//获取堆的size
int HeapSize(Heap* pheap);

1. 堆初始化与销毁

初始化

c 复制代码
//堆初始化
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的倍数)

销毁

c 复制代码
//清理资源
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. 元素交换函数

c 复制代码
void Swap(HeapDataType* child, HeapDataType* parent) {
	HeapDataType temp = *child;
	*child = *parent;
	*parent = temp;
}

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

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


3. 堆化操作

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

向上调整(子→父)

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

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

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

终止条件

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

向下调整(父→子)

c 复制代码
// 向下调整 到叶子结点结束,叶子结点的左孩子 的下标  大于 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;
		}
		else
			break;
	}
}

实现思路

  • 断言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)

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


4. 堆元素插入

  • 向上调整的过程
c 复制代码
// 插入,不能指定位置插入。
// 因为新元素插入后要进行调整使其满足堆的结构,指定的位置不一定是最终调整后的位置
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. 堆元素删除

  • 向下调整的过程
c 复制代码
//堆的删除  应当删除堆顶的元素,删除堆尾的数据没有意义。
//删除最大的或最小的,可以选出第二大或第二小的
//挪动删除(直接删)的缺点:  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. 辅助功能函数

堆的判空

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

获取堆顶元素

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

获取堆的大小

c 复制代码
//获取堆的size
int HeapSize(Heap* pheap) {
	assert(pheap);
	return pheap->size;
}

功能说明

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

结语

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

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

相关推荐
躺不平的理查德31 分钟前
C++-Lambda表达式
开发语言·c++
qq_447429411 小时前
数据结构与算法:图论——并查集
数据结构·算法·图论
兮山与1 小时前
数据结构4.0
java·开发语言·数据结构
庐阳寒月1 小时前
linux多线(进)程编程——(10)信号
linux·c++·嵌入式
孞㐑¥1 小时前
Linux之基础开发工具(yum,vim,gcc,g++)
linux·c++·经验分享·笔记
学习中的农民工1 小时前
Android ndk 编译opencv后部分接口std::__ndk1与项目std::__1不匹配
android·c++·opencv
一道秘制的小菜1 小时前
Linux第20节 --- inode和文件系统
linux·运维·服务器·c++·文件
迪小莫学AI2 小时前
# LeetCode 1007 行相等的最少多米诺旋转
windows·python·leetcode
0509152 小时前
计算方法实验四 解线性方程组的间接方法
线性代数·算法·数学建模·矩阵·动态规划
算法歌者5 小时前
[C]基础14.字符函数和字符串函数
c语言