【数据结构和算法】10.堆的模拟故事:用图文解析讲述它的成长之旅

  欢迎来到 CILMY23 的博客

🏆本篇主题为:堆的模拟故事:用图文解析讲述它的成长之旅

🏆个人主页:CILMY23-CSDN博客

🏆系列专栏:Python | C++ | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算法专题 | 代码训练营

🏆感谢观看,支持的可以给个一键三连,点赞收藏+评论。如果你觉得有帮助,还可以点点关注


前言: 

Hello,大家好,这里是 CILMY23 的频道,上期我们接触了树,知道了堆(Heap)总是一棵完全二叉树,今天我们就来深入探究一下堆的模拟实现。

个人分享:每个人的内心都是一片海洋,每一路人都如此。每个人都是思想、见解和情感的宇宙。


目录

堆的模拟实现

思维导图

堆的结构

堆的初始化和销毁

堆的插入 

扩容

 插入

向上调整 

堆的删除

 方法1:

方法2:

堆顶数据获取

 其余接口

判断是否为空堆

堆的大小


堆的模拟实现

思维导图

我们大致按照这样的顺序来实现堆

 

堆的结构

 我们之前说过堆在逻辑结构上是一个完全二叉树,但是物理结构上是一个数组,所以我们按照顺序表的那种形式设计。

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int _size;
	int _capacity;
}HP;

 底层上看,虽然是一个动态顺序表,但是是顺序表吗?

不是,因为逻辑结构上看,我们要当成一个完全二叉树来实现。而且还要想办法调整成大堆或者小堆。

顺序表对进来的值没有要求,但是堆对进来的值就有要求了。我们要求

小堆(小根堆):任意一个父亲都小于等于孩子

大堆(大根堆):任意一个父亲都大于等于孩子

所以本文主要以小堆的实现为主。 

堆的初始化和销毁

堆的初始化和销毁比较简单,本文就不多阐述,直接上代码。

//堆的初始化
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->_size = php->_capacity = 0;
}

//堆的销毁
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);

	php->a = NULL;
	php->_size = php->_capacity = 0;
}

堆的插入 

因为我们是根据小堆来设计的,所以对数值进入小堆的要求有设定。首先,在插入之前要扩容一下,这里我们可以封装这个扩容函数,也可以不封装扩容函数。

因为这里的扩容只会在push用一次,我们之前封装函数是为了多次调用,形成模块化设计,简洁代码,一般对重复多的才使用扩容函数,那这里我就封装一下,根据个人习惯来。

扩容

记得realloc 前强转对应的数据类型。 

//扩容
void HPCheckCapacity(HP* php)
{
	assert(php);

	if (php->_size == php->_capacity)
	{
		int newcapacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->_capacity = newcapacity;
	}
}

 插入

插入分很多情况,因为这是小堆,需要父亲比子结点小,如果我们第一次插入的情况,是比父结点大的,那我们就不动它。就比如下面这种情况,我们插入的值是30, 

第二种插入情况,当我们插入的值比父亲小,但是比父亲的父亲也就是爷爷大,这时候我们就要和父亲进行交换。如图所示:

第三种情况,当我们插入的值是最小的,这时候就得一直交换下去,直到根结点。

 因此,这种不断调整的情况,我们把这个过程叫做向上调整,跟祖先不断进行比较。所以整出了一个向上调整算法。

向上调整 

 向上调整的代码可以单独封装一个函数(AdjustUp),因为我们会经常使用,并且还会经常交换(Swap),所以这两个都可以封装一个函数。

向上调整的过程无非就二点,判断,交换。

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

在写循环的时候,可能很多同学会把条件写成下面这种形式:

这个条件会不会有问题,parent会不会小于0?

:会有问题,parent最后落的位置不可能是小于0.

0-1/2最后还是0,但是这里也有偶然, 因为child刚好也是0,就循环结束了。

更好的循环条件应当是:child 大于 0

这里我还出错了,大家注意在传结构体指针a的时候,不要再取地址了,那样就变二级指针了。(血的教训)

递归写法如下(没大批量测试过,仅供参考):

//向上调整递归写法
void AdjustUp(HPDataType* a, int child)
{
	if (child <= 0)
		return;

	int parent = (child - 1) / 2;
	if (a[child] < a[parent])
	{
		Swap(&a[parent], &a[child]);
		AdjustUp(a, parent);
	}
}

向上调整可不可以用递归?

可以,但是我们不用递归,一般情况下。为什么呢?如果能用循环写,就没必要用递归。递归缺点如下:

1.递归消耗大,因为要不断建立栈帧

2.递归维护上难度大。

为什么链式二叉树用递归了?

因为链式二叉树的非递归更难,本身更适合用递归。

 总结最后的代码如下:

//堆的插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	HPCheckCapacity(php);

	php->a[php->_size] = x;
	php->_size++;

	AdjustUp(php->a, php->_size - 1);
}

堆的删除

 方法1:

大部分人理解都是从末尾删,看起来没影响。可不可以呢?可以,但是这是一种偷懒的行为。而堆的pop的规定是删堆顶的数据。堆顶其实也就是根节点。

删堆顶有什么意义?

:因为小堆的堆顶是最小值,大堆则是最大值。删完后堆顶是次小值(次大值),这样可以选数,选大和选小,这样后面就可以排序。

而插入删除的前提:都是堆,如果我们选择挪动数据覆盖,删除根的话,就有可能不是一个堆了。

如图所示,如果十九和十八换了一个位置,这个就构不成堆。 

 

 总结下来就是

1.挪动数据覆盖,这个时间复杂度是O(N)

2.挪动数据后,整棵树的父子关系全乱了,也就是大小关系都乱了。

方法2:

 第一步,首尾交换,然后尾删,这么做的好处是,数组或者说是顺序表,它的尾删的代价很小。 

如图所示:先把十五和三十交换了,然后进行尾删,这个时候因为顺序被打乱了,所以我们需要调整。

 第二步:向下调整的算法,也就是小的往上浮,大的往下沉。通过logN我们就可以选出次小的数据。

这里最主要的一步是这个假设法,在进行推理的过程中会发现一个难点,就是往下调整的时候会遇到左孩子和右孩子,不知道该和谁交换。

我们要保证是小堆,只需要保证父结点小于等于孩子结点即可,有序不是我们考虑的范围。

所以我们可以利用一个假设法,假设这个孩子一直是最小的那个,可以先让孩子从左孩子开始,如果右孩子比左孩子小,那么就让孩子变成右孩子,这样反复下去,我们只有一个孩子,只是这个孩子是在这两个左孩子和右孩子中跳转,它永远选择最小的那个孩子。

 代码如下:

最极端的情况就是我们走到底了,我们要保证孩子在这个size的范围内。

//向下调整
void AdjustDown(HPDataType* a, int size,int parent)
{
	//假设法
	int child = parent * 2 + 1;

	while (child < size)
	{
		//先假设左孩子小,假设错误了,就+1变右孩子。
		if (child + 1 < size && a[child + 1] < a[child])
		{
			child++;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

总结:

//堆的删除
void HeapPop(HP* php)
{
	assert(php);
	assert(php->_size > 0);

	//首尾交换
	Swap(&php->a[0], &php->a[php->_size - 1]);
	php->_size--;

	AdjustDown(php->a, php->_size, 0);
}

堆顶数据获取

堆顶数据获取呢比较简单,主要是数组的0位置就是堆顶。 

//堆顶数据获取
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->_size > 0);

	return php->a[0];
}

玩法1:

这个玩法还是很有意思的,这样可以快捷找到前k个数据。大堆就可以快捷找到最大的前k个了。

起找出最小的前k个: 

//删除的玩法1
//找出最小的前k个
int k = 3;
while (k--)
{
	printf("%d ", HeapTop(&hp));
	HeapPop(&hp);
}

 其余接口

判断是否为空堆

//判断堆是否为空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->_size == 0;
}

堆的大小

//堆的大小
size_t HeapSize(HP* php)
{
	assert(php);

	return php->_size;
}

 玩法2:

这里同样也有一个玩法:

遍历整个堆。

//玩法2:
while (!HeapEmpty(&hp))
{
	printf("%d ", HeapTop(&hp));
	HeapPop(&hp);
}

至此,我们堆的模拟实现就结束了。 


 🛎️感谢各位同伴的支持,本期堆的模拟实现就讲解到这啦,下期我们将进入xxxxx,如果你觉得写的不错的话,可以给个一键三连,点赞,收藏+评论,可以的话还希望点点关注,若有不足,欢迎各位在评论区讨论。       

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值