1、堆的概念及结构
1.1堆的概念
如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足ki<=k2i+1且ki<=k2i+2(或满足ki>=k2i+1且ki>=k2i+2),其中i=0,1,2,…,则称该集合为堆。
小堆:每个父节点不大于子节点
大堆:每个父节点不小于子节点
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
1.2堆的结构
堆的逻辑结构是棵完全二叉树,堆在物理结构上是一个一维数组,所以堆的本质是一个数组。堆又分为大根堆和小根堆。
大根堆:每个父节点大于等于子节点
2、堆的实现
堆的实现跟顺序表的实现本质上 是一样的,对堆进行一系列初始化、插入、删除、销毁等操作。但是也有不同之处,就是要实现堆需要两个算法,一个是向上调整算法,一个是向下调整算法。
我们先搞一下堆的向上调整算法和向下调整算法
2.1堆的向上调整算法
向上调整算法经常用在堆的插入中,我们插入数据只能在堆的尾部插入,但是插入之后,堆的性质就会发生变化,就需要我们将插入的尾部数据顺着双亲向上一步一步的调整,成为新的堆。
比如现在有一个小堆,我想插入一个6进去。第一步就是将数据6插入到小堆的尾部,也就是最后一个孩子的后面。
当6插进去之后,你会发现堆的性质发生改变,原来是一个小根堆,每个父亲节点都小于或等于自己的子节点,但插入6之后,就不满足这个性质了,所以就需要我们将6顺着双亲往上调整,让他恢复原本小堆的性质,那么该怎么进行调整呢?这就是我们的第二步了。
我们只需要让插入的新结点与它的父节点进行比较,如果插入的这个节点大于等于它的父节点就不用做改变。如果插入的这个节点小于他的父节点,就让他们两个的节点值交换位置。交换完之后,还需要让父节点与他的父节点进行比较,如果该父节点也小于它的父节点,那就也让这个父节点向上调整,直到调整到堆顶的位置。
注意:未插入数据时,必须是个堆。
代码演示
child = parent * 2+1;
parent = (child-1)/2;
void AdjustUp(HeapDataType* a, int child)//child是下标,插入的尾元素的下标
{
//记录插入数据的父节点
int parent = (child - 1) / 2;
while (child > 0)
{
//以小堆为例
if (a[child] < a[parent])
{//如果父节点大于子节点了,交换两个节点的值
Swap(&a[child], &a[parent]);
//交换后接着比,孩子到父亲上,成为原祖父的儿子
child = parent;
//更新到新的父亲
parent = (child - 1) / 2;
}
else
{
break;//父亲节点小于子节点,已经成为堆了,不用再比较了。
}
}
}
2.2堆的向下调整
堆的向下调整经常用到堆的删除操作中,也就是用来删除堆顶元素。向下调整的前提是:除了堆顶元素外,左右子树必须满足堆的性质,也就是左右子树要么全是大堆,要么全是小堆。
除了堆顶元素37不满足小堆的性质外,其它的元素都满足。
以小堆为例:
那么如何将他调成小堆呢?这就需要我们进行向下调整了。
算法思路:
小堆:找子结点中相对较小的子节点与父节点进行比较 ,如果父节点大于子节点,则两者交换。
大堆:找子结点中相对较大的子节点与父亲比较,如果父节点小于子节点,则进行交换。
堆的向下调整算法,最坏的情况(一直需要交换节点),需要向下调整h-1次,(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
上面说到,使用向下调整算法,需要满足其的左右子树均为大堆或者小堆,那么我们怎么才能将任意一个数转化为堆呢?我们只需要从倒数第一个非叶子节点开始,从后往前,按下标,作为根,依次向下调整就可以了。
2.3两个数值交换
这个思想大家应该都不陌生
void Swap(int* child, int* parent)
{
HeapDataType tmp = *child;
*child = *parent;
*parent = tmp;
}
2.4堆的初始化
堆的本质:物理上操纵的是数组,逻辑上操纵的是树。所以堆的初始化跟之前的顺序表差不多,就是把数组置空,数组的容量和有效元素个数都为0。(为什么要断言呢?)
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->size = 0;
}
2.5堆的销毁
堆的销毁也是跟之前的顺序表一样,因为他们的本质都是数组。所以要销毁堆的话,直接把数组释放掉,再把数组置为空,数组的有效元素个数和容量都为0.
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
2.6堆的插入
插入一个元素,你要看他的容量是否满了,如果满了,那就扩容,扩完容再插入。没满那就直接插入。插入之后,元素的有效个数就多了一个(size++)。
需要注意的是,这是堆,堆有大堆和小堆。插入元素之后,会有两种情况:一是插入之后堆不受影响,就是直接插入了,该是大堆还是大堆,该是小堆还是小堆。二是插入之后,不是堆了,需要进行向上调整。
void HeapPush(HP* php, HeapDataType x)
{
assert(php);
//如果空间满了
if (php->size == php->capacity)
{//如果空间为0,那就开辟四个字节。空间不为0,那就开辟2*capacity个字节
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HeapDataType* tmp = (HeapDataType*)realloc(php->a, newCapacity * sizeof(HeapDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;//把x插入
php->size++;//成功插入数据x,有效元素个数加一
AdjiustUp(php->a, php->size - 1);//向上调整算法
}
2.7堆的删除
堆的删除操作需要用到向下调整。堆的删除是要删除哪一个数据呢?堆的删除删除的是堆顶数据。因为删除堆尾数据没有意义,堆尾的数据不一定是最大的,也不一定是最小的,所以没有任何意义。
大堆:堆顶数据便是最大值。当最大值删除后,我们才可能获得次大的,然后是次次大的,以此类推。
小堆:小堆的堆顶数据是最小的。当堆顶数据删除后,我们可以获取次小的,然后是次次小的,依次类推。
那我们该怎么删除堆顶数据呢?如果直接将堆顶数据删除,剩下的数据就会混乱,那我们该怎么操作呢?
第一步:将堆顶数据与堆尾数据进行交换;
第二步:删除堆尾数据
第三步:让交换上去的堆顶数据向下调整,恢复堆的性质。
这样做堆的左右子树也不会有太大的变化。
void 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);//向下调整数据
}
2.8获取堆顶数据
HeapDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];//获取堆顶数据
}
2.9堆的数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
2.10堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}