堆的物理结构和逻辑结构是什么?
堆如何插入数据和删除数据?为什么?
向上调整和向下调整的要求是啥?
文中不理解的可以先看堆的代码和基础知识-CSDN博客
也欢迎评论区一起讨论
1.堆的物理结构和逻辑结构
我们的堆是用数组实现的
因此堆的物理结构是数组
但是堆的逻辑结构确是二叉树
2.插入数据和向上调整
但是我们再思考一个问题就是,当我们有一个堆的时候,怎么是插入数据呢?
以上面画的那个图为例,我们要插入一个 数据,并且还要保持堆的特性(父节点大于子节点)
我们该如何调整呢?
但是我们要首先要明确一个点就是除了我们插入数据,其他数据,其他数据成堆
这个就是向上调整的使用条件
设我们要插入数据,只能在物理结构上插入到数组的最后一个,再对插入的数据进行调整
也称为向上调整
这样的好处是我们不会对前面的数据产生影响(前面的数据成堆)
然后我们要进行调整了
这个地方假设我们插入的是99,明显就需要调整,但是只有这个数据是有问题的,因此我们只用对这个数据和它的祖先进行遍历调整就可以了
那么遍历什么时候停止呢?
1.就是如上图,我把插入节点的祖宗节点全部遍历完(这个就是上面那个图停止的原因)
2.就是当插入的数据符合堆的要求(父节点大于子节点)
那么我们就可以开始写代码了
首先就是子节点和父节点的关系是什么呢?
所以
父节点=(子节点-1)/2
我们就可以开始写向上调整的代码了
// 除了child这个位置,前面数据构成堆
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)//判断遍历是否结束
{
if (a[child] > a[parent])//不满足堆的要求,进行调整
{
Swap(&a[child], &a[parent]);//交换父子节点
child = parent;
parent = (child - 1) / 2;
}
else//满足,调整结束
{
break;
}
}
}
我们思考一下就是如果改成while(parent>=0)是否也可以
答案是未必
当child等于0的时候parent=(child-1)/2=-1/2=0;
这个时候我们遍历结束但无法跳出while循环了
我们向上调整写完了,那么我们就可以接着写如何插入数据了,那就非常easy了
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
上面大部分代码基本都是老朋友了,如果不熟那就先去复习一下前面的链表和顺序表,这个地方我解释一下,这个地方向上调整这个函数为啥要传php->size-1而不是php->size
我们先来看向上调整函数里面我们交换的是什么?
Swap(&a[child], &a[parent]);
交换的是两个数组的地址啊,数组的下标是从0开始的,但是size代表的是节点个数,是从1开始的啊,所以这个地方child的下标就是size-1了
3.删除节点删什么节点
这个地方我们以大端为例,小端同理
我们接下来再看怎么删除节点,我们先来想想我们删除节点是删除叶节点还是端节点?
当然肯定是端节点啊,为什么啊?
首先我们要思考一个问题,就是我们的堆是用来干嘛的?
其中很重要一个作用是用来排序的啊!
那么我们堆满足的性质是什么?
是所有的父节点都大于子节点
但是我们能不能保证兄弟节点谁大谁小啊,这个是未知的
但是如果我们要排序我们去删除叶节点,我们怎么确定这个叶节点是最大的节点啊(你怎么知道它的兄弟节点一定比它大啊?)
这样我们无法达到排序的功能
但是我们的端节点,它没有兄弟节点,只有子孙节点,这也就意味着它是所有数里面最大的,我把端点删了,得到的下一个端点不就是第二大的数吗。
以此类推,我们就可以得到第三大,第四大等等的数了吗?不就可以实现排序了吗?
4.删除节点和向下调整
但是这个地方我们要记住一个点,后面会用上,就是我们把端点删除后,它的左右子孙端点会形成新的堆
这个也就是向下调整的使用前提
那我们思考一下,我们删除了端点,接下来改怎么调整呢?
端点删除之后的左右子孙端点会形成新的堆,那我们最好就不要破坏这个结构,
就不要直接把这两个端点最大的那个设为新的端点,因为这样有可能会导致后面的数据不构成堆,会非常麻烦,我们就直接把最后一个节点和端点交换,
然后再对端点进行free就完成删除了,那么删除后,我们还要对这个堆进行调整,这个时候就是向下调整了
向下调整这个地方我们以大堆为例子
我们把端点和叶节点交换位置后, 我们要调整,要保证父节点大于子节点,
因为我们只用把交换的那个数据 从上到下 和 其子节点最大的值 进行比较,如果比它们小,那就交换位置
,遍历一遍就可以了
停下来有两个条件
1.第一就是遍历完了,和叶节点比较完了
2.第二就是满足这个数据大于它的两个子节点最大的一个了(上面画的图属于这种)
因此我们就可以写向下调整的代码了
// 左右子树都是大堆/小堆
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 选出左右孩子中大的那一个
if (child + 1 < n && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
同样,删除端点的代码也可以写了
oid HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
// 删除数据
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
这个地方的size-1而不是size的原因见上文