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堆的向上调整算法
我们为什么要用到堆的向上调整算法呢?当我们在堆的尾部插入一个数据时,堆的性质可能会发生改变。那就需要我们用堆的向上调整算法,对其进行调整,恢复堆的性质。
对堆进行向上调整,是从最后一个叶子节点开始,下标从后往前,一个一个的与其父亲节点进行比较,如果父亲节点大于子节点,则交换位置,就这样挨个比较,直到父亲节点小于子节点或者遍历到根节点为止。
void AdjustUp(HPDataType* a, int 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堆的向下调整算法
我们为什么会用到堆的向下调整算法呢?因为在删除堆顶元素时,我们需要交换第一个数据与最后一个数据的值(详细后面会讲),交换过后,堆的性质也就发生了改变,就需要我们去调整。怎样去调整呢?这就需要我们的向下调整算法了,从根节点位置,往下调整。
根的向下调整算法需要满足一个前提:
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。
我们现在给出一个数组,在逻辑上看成一个二叉树,然后从根节点开始,用向下调整算法向下调整整。
基本思想:
1、从根节点处开始,选出左右孩子中较小的孩子;
2、让较小的孩子与父亲进行比较
如果父亲节点大于孩子,那让孩子与父亲的数值进行交换。并将原来孩子的位置当成父亲继续进行向下调整,直到调整到叶子结点为止。
若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。
void AdjustDown(HPDataType* a, int n, int parent)
{
//记录较小子节点的位置
int child = parent * 2 + 1;
//到堆尾结束循环
while (child < n)
{
//如果存在右孩子且左孩子的值大于右孩子的值
if (child + 1 < n && a[child] > a[child + 1])
{
//记录较小孩子的位置
++child;
}
//父结点的值大于较小孩子的值
if (a[child] < a[parent])
{
//交换父节点和较小孩子的值
Swap(&a[child], &a[parent]);
//继续向下调整
parent = child;//父节点爬到孩子所在的位置,也就是下一次调整的父节点
child = parent * 2 + 1;//记录下一次调整的孩子节点
}
else//堆已建好
{
break;
}
}
}
堆的向下调整算法的时间复杂度
最坏的情况是向下调整(h-1)次,h为书的层数。树的节点个数为N,树的节点个数的范围为[2^(h-1),2^h-1],所以h = log(N+1)。所以向下调整的时间复杂度为O(logN)。
3、堆的创建
3.1堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
3.2堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
3.3堆的插入
插入一个数据,是在数组的末尾插入,即是在树的最后一层最后一个节点插入,插入数据后,堆的性质可能会发生改变,这就需要我们对堆进行向上调整。
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
else
{
php->a = tmp;
php->capacity = newCapacity;
}
}
php->a[php->size] = x;//插入x
php->size++;//元素个数加一
//向上调整
AdjustUp( php->a, php->size - 1);
}
3.4堆的删除
堆的删除删除的是堆顶数据。堆顶数据是数组的最大值或者最小值,这样的值有意义。但是堆的删除并不能直接删除堆顶数据。首先需要我们将堆顶数据和堆尾数据进行交换,然后 再删除堆尾数据,最后再向下调整就可以了。
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);
}
3.5获取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
3.6获取堆的有效个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.7判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
每日一句
If you shed tears when you miss the sun, you also miss the stars.
如果你因错过太阳而流泪,那么你也将错过辰星。