数据结构 之 【堆】(堆的概念及结构、大根堆的实现、向上调整法、向下调整法)(C语言实现)

目录

1.堆的概念及结构

1.1堆的概念

1.2堆的结构

2.堆的实现(以大堆为例)

[2.1 前提准备](#2.1 前提准备)

2.2初始化

2.3销毁及交换函数

2.4插入及向上调整法

向上调整法

2.5删除及向下调整法

向下调整法

2.6判空、堆顶元素、元素个数


这里的堆和操作系统虚拟进程地址空间中的堆是两回事,

一个是数据结构,一个是操作系统中管理内存的一块区域分段

1.堆的概念及结构

1.1堆的概念

一棵父亲节点的值总是不大于其子节点值的完全二叉树叫作小根堆(最小堆)

一棵父亲节点的值总是不小于其子节点值的完全二叉树叫作大根堆(最大堆),

(后续简称小堆、大堆)

1.2堆的结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费

而完全二叉树适合使用顺序结构存储,因为其每层节点从左至右连续排列,无间隔缺失

堆既然是一种完全二叉树,那就可以使用顺序结构的数组进行存储,即

按照从上至下从左至右的数组顺序对所有节点的值进行存储

这样,既不浪费空间,又能够高效率的遍历数据

由上图我们可以看到数组结构可以很好的模拟完全二叉树

注意:

逻辑结构是指我们想象出来的结构,物理结构是指数据在内存中实际存储时的结构

堆只要求了父亲节点与子节点值之间的关系,并没要求兄弟节点、堂兄弟节点值之间的关系

2.堆的实现(以大堆为例)

前面讲了二叉树的顺序存储结构更适用于完全二叉树,

所以实现堆就是在操作数组的基础上实现堆的性质

2.1 前提准备

复制代码
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

typedef int HpDateType;
typedef struct heap
{
	HpDateType* a;
	int size;
	int capacity;

}HP;

1.包含相关头文件;2.为了便于修改数据类型重命名一下

3.定义结构体,在堆上开空间来存储相关数据

2.2初始化

复制代码
//初始化
void HpInit(HP* php)
{
	//php指向一个堆,不能为空
	assert(php);

	php->a = (HpDateType*)malloc(sizeof(HpDateType) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	php->size = 0;
	php->capacity = 4;
}

void HpInitArray(HP* php, HpDateType* a, int n)
{
	assert(php);

	php->a = (HpDateType*)malloc(sizeof(HpDateType) * n);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	memmove(php->a, a, n * sizeof(HpDateType));
	php->size = n;
	php->capacity = n;

	//建堆
	for (int i = (n - 2) / 2; i >= 0; --i)
	{
		AdjustDown(php->a, php->size, i);
	}
}

(1)无参初始化目的是预开一定的空间,

利用给定数组初始化目的是直接根据数组数据进行建堆(后续讲解)

(2)所给的堆结构体指针变量不应为空,所以断言一下

2.3销毁及交换函数

复制代码
void HpDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}
//交换
void Swap(HpDateType* p1, HpDateType* p2)
{
	HpDateType temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

(1)销毁时需正确释放空间并更新指针及其余变量

(2)后续频繁使用交换操作,这里单独设定一个函数

2.4插入及向上调整法

复制代码
void HpPush(HP* php, HpDateType x)
{
	assert(php);
	//扩容
	if (php->capacity == php->size)
	{
		HpDateType* temp = (HpDateType*)realloc(php->a, sizeof(HpDateType) * php->capacity * 2);
		if (temp == NULL)
		{
			perror("malloc fail");
			return;
		}

		php->a = temp;
		php->capacity *= 2;
	}
	//实现插入,插入到末尾,实现大堆
	//需要依次与父亲进行比较,即向上调整
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size - 1);
}

(1)插入数据第一步,思考扩容

(2)顺序存储结构中,尾插效率更高,先将数据插入到尾部

(3)为了实现大堆,需要将所插入的数据进行调整,即AdjustUp();

向上调整法

向上调整法的前提是数据插入前本身就是一个堆

逻辑结构上

如果所插入数据比其祖先节点的值大就进行交换,否则就不动,最终实现大堆

具体操作体现在物理结构,即数组上

通过下标间的关系,parent = ( child - 1 ) / 2(计算机整除运算自动向下取整)

依次找到尾节点的祖先节点,然后比较数据大小,大就交换,小就停止交换

复制代码
    int parent = (child - 1) / 2;
while ()
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

这显然是一个循环!
通过前面的分析,我们很容易知道, 情况最坏是,所插入数据需要与根节点的值进行交换 ,自然地,就会把 parent(父亲节点下标)大于等于0作为循环结束条件,但正如上述代码所展示的一样,通过迭代更新父子节点的下标, parent恒大于等于0(最坏情况当child等于0时也成立),也就是说如果循环内部不能控制住结束条件,这将是一个 死循环
观察到, 所插入数据与根节点的值进行交换后 ,child 更新为 0 , parent仍等于0 那么就将 child > 0 作为循环结束条件

复制代码
void AdjustUp(HpDateType* a, int child)
{
	//给定数组为空时,没必要进行下面步骤
	assert(a);

	int parent = (child - 1) / 2;

	//parent恒大于等于0
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

2.5删除及向下调整法

复制代码
void HpPop(HP* php)
{
	assert(php);
	//assert(!HpEmpty());

	//删除数据,只删除堆顶的数据
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	//向下调整,实现大堆
	AdjustDown(php->a, php->size, 0);
}

(1)数组中,尾删 的效率较高,其余位置的删除操作都会因为移动数据造成效率低下

(2)堆的删除操作是针对堆顶元素的删除,原因如下:

在大堆中,最大元素位于堆顶,删除之后通过向下调整法(后续讲解) 次大元素就会位于堆顶

这样的删除更具功能性,更有利于在现有基础上直接找到最大元素

(3)具体操作就是先将堆顶元素与最后一个数据元素进行交换,然后删除堆顶元素,

最后对余下数据进行向下调整操作,使其重新成为一个大堆

向下调整法

向下调整法的前提是所调整节点的左右子树是一个堆

逻辑结构上

如果该节点数据比其孩子节点中的较大值小就进行交换,否则就不动,最终实现大堆

具体操作体现在物理结构,即数组上

通过下标间的关系,leftchild = parent * 2 + 1(计算机整除运算自动向下取整)

rightchild = leftchild + 1,依次找到该节点的孩子节点

然后与孩子节点中的较大值进行比较,大就交换,小就停止交换

复制代码
int child = parent * 2 + 1;

while ()
{
	//左孩子一个逻辑,右孩子一个逻辑,直接假设
	//child是左右孩子中较小的一个孩子的下标
	//注意右孩子的有无(防止越界访问与无效数据)
	if (child + 1 < n && a[child] > a[child + 1])
	{
		++child;
	}
	//与左右孩子中较大的一个孩子进行比较
	if (a[parent] > a[child])
	{
		Swap(&a[parent], &a[child]);
		parent = child;
		child = parent * 2 + 1;
	}
	else
	{
		break;
	}
}

这显然是一个循环!
向下调整时,

(1)父亲节点有孩子节点时,就需要与孩子节点的值进行一次比较,所以 左孩子节点的下标小于元素个数 ,即 child < n可作为循环结束条件

(2)交换操作的实质是如果 父亲节点值比孩子节点中的较大值小,就进行交换,否则就不动

那么我们就先假设左孩子节点的值较大,再将左孩子节点的值与右孩子节点值进行比较,更新孩子节点,然后与父亲节点的值进行比较,从而简化比较交换操作

需要进行比较然后进行相同的操作时,先假设再判断更新的方法更好

(3)注意右孩子的有无(防止越界访问与无效数据)

复制代码
if (child + 1 < n && a[child] > a[child + 1])

必须先判断右孩子的有无,再进行访问操作,否则就是越界访问或无效数据!

复制代码
void AdjustDown(HpDateType* a, int n, int parent)
{

	int child = parent * 2 + 1;
	//有左孩子才进行调整
	while (child < n)//n是节点个数
	{
		//左孩子一个逻辑,右孩子一个逻辑,直接假设
		//child是左右孩子中较小的一个孩子的下标
		//注意右孩子的有无(防止越界访问与无效数据)
		if (child + 1 < n && a[child] > a[child + 1])
		{
			++child;
		}
		//与左右孩子中较大的一个孩子进行比较
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

2.6判空、堆顶元素、元素个数

复制代码
//判空
bool HpEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
//堆顶元素
HpDateType HpTop(HP* php)
{
	//assert(php);
	//一举两得
	assert(!HpEmpty(php));

	return php->a[0];
}
//大小
int HpSize(HP* php)
{
	assert(php);

	return php->size;
}

(1)注意相关断言

(2)根据意义直接返回相应值即可

相关推荐
clock的时钟1 小时前
暑期数据结构第一天
数据结构·算法
小小小小王王王1 小时前
求猪肉价格最大值
数据结构·c++·算法
SuperW4 小时前
数据结构——队列
数据结构
??tobenewyorker4 小时前
力扣打卡第二十一天 中后遍历+中前遍历 构造二叉树
数据结构·c++·算法·leetcode
让我们一起加油好吗4 小时前
【基础算法】贪心 (二) :推公式
数据结构·数学·算法·贪心算法·洛谷
蓝澈11214 小时前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
呆瑜nuage7 小时前
数据结构——堆
数据结构
蓝澈11217 小时前
弗洛伊德(Floyd)算法-各个顶点之间的最短路径问题
java·数据结构·动态规划
127_127_1277 小时前
2025 FJCPC 复建 VP
数据结构·图论·模拟·ad-hoc·分治·转化