欢迎来到 CILMY23 的博客
🏆本篇主题为:堆的模拟故事:用图文解析讲述它的成长之旅
🏆个人主页:CILMY23-CSDN博客
🏆系列专栏:Python | C++ | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算法专题 | 代码训练营
🏆感谢观看,支持的可以给个一键三连,点赞收藏+评论。如果你觉得有帮助,还可以点点关注
前言:
Hello,大家好,这里是 CILMY23 的频道,上期我们接触了树,知道了堆(Heap)总是一棵完全二叉树,今天我们就来深入探究一下堆的模拟实现。
个人分享:每个人的内心都是一片海洋,每一路人都如此。每个人都是思想、见解和情感的宇宙。
目录
堆的模拟实现
思维导图
我们大致按照这样的顺序来实现堆
堆的结构
我们之前说过堆在逻辑结构上是一个完全二叉树,但是物理结构上是一个数组,所以我们按照顺序表的那种形式设计。
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,如果你觉得写的不错的话,可以给个一键三连,点赞,收藏+评论,可以的话还希望点点关注,若有不足,欢迎各位在评论区讨论。