唤起“堆”的回忆
我们一起见证堆是怎么形成的吧!
目录
堆的概念:
堆的结构:
堆的实现:
向下调整法:
向上调整法:
堆的初始化:
堆的插入:
堆的删除:
堆的判空和有效个数:
堆的销毁:
堆的打印:
结语:
堆的概念:
堆总是一棵完全二叉树,如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足ki\u003C=k2i+1且ki\u003C=k2i+2(或满足ki>=k2i+1且ki>=k2i+2),其中i=0,1,2,…,则称该集合为堆;堆又分为大堆和小堆
大堆:将根节点最大的堆叫做最大堆或大根堆
小堆:将根节点最小的堆叫做最小堆或小根堆
堆的结构:
堆有两种结构,分别为存储结构和逻辑结构,存储结构通常把堆(二叉树)用数组来存储,逻辑结构则是按照二叉树的模样展示,如图:
(上面逻辑结构结点相连,忘记连了,不好意思)
堆的实现:
堆是由数组构成,我们首先需创建一个数组,我们以建小堆为例,我们需要通过我们需要进行向下调整,但我们调整时需满足一个前提——左右子树是堆,并都为小堆
向下调整法:
怎么调?我们可以观察下面这张图(假设数组int a[6] = {8,2,5,6,7,4}):
由上图可知,我们先找比较根结点的左右孩子大小,让小的孩子再与父亲大小比较
若小的孩子比父亲小,则进行位置交换,并将原来孩子的位置当成父亲继续往下比较
若小的孩子比父亲大,则不做处理
void Swap(int* p, int* q)//交换值
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void Adjustdown(int* a, int n, int parent)
{
int child = 2 * parent - 1;
if (a[child] > a[child + 1])//选出较小的孩子
{
++child;
}
while (child < n)
{
if (a[parent] > a[child])//父亲与孩子比较
{
Swap(&a[parent], &a[child]);
//更换父结点的位置
parent = child;
child = 2 * parent - 1;
}
else
{
break;
}
}
}
但这个有局限性,左右子树需为小堆或大堆,随便给个数组,我们又该怎样建堆呢?我们可以找到最后一个非叶子的数,然后从下往上依次向下调整,如图(以升序建大堆为例):
for (int i = (n - 1 - 1) / 2; i >= 0; i--)建大堆
{
Adjustdown(php->a, php->size, i);
}
向上调整法:
当我们需要插入一个数据(叶结点)时,需要对堆进行调整,使其满足堆的条件,我们需要用到向上调整法,如图(建小堆为例):
首先将要插入的结点和它所属的父结点进行比较
若要插入的结点比其父结点小,则交换位置,并将要插入的结点继续向上调整,若要插入的结点比其父结点大,则不再进行处理
void Swap(int* p, int* q)//交换值
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void Adjustup(int* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//孩子为根结点循环终止
{
if (a[parent] > a[child])//父亲与孩子比较
{
Swap(&a[parent], &a[child]);
//更换父结点的位置
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
堆的初始化:
我们先需构建一个类型,创建一个动态数组,大小和容量
void HpInit(Hp* php, HpDataType* a, int n)
{
assert(php);
HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType));//开辟空间
if (tmp == NULL)
{
perror("malloc fail");
return;
}
a = tmp;
php->size = n;//令大小为n
php->capacity = n;
}
堆的插入:
我们先进行尾插,然后前面提到了插入数据需要向上调整,这样不会破坏堆的性质,在向上调整
void CheckCapacity(Hp* 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;
}
}
void HpInsert(Hp* php, HpDataType x)
{
assert(php);
//检查容量
CheckCapacity(php);
//尾插
php->a[php->size] = x;
php->size++;
//向上调整
Adjustup(php->a, php->size - 1);
}
堆的删除:
删除是删除堆顶的元素,但不能直接删除,这样会破坏堆的性质,我们要先将堆顶元素与最后一个结点的元素进行交换,然后删除最后一个结点的元素,最后进行向下调整,保证得到一个正确的堆
void HpErase(Hp* php)
{
assert(php);
if (php->size == 0)//如果堆为空,则不删除
return;
else
{
Swap(&php->a[0], &php->a[php->size - 1]);//交换堆顶元素和最后一个元素
php->size--;//删除最后一个元素
Adjustdown(php->a, php->size - 1, 0);//向下调整
}
}
堆的判空和有效个数:
如果堆的大小为0,则说明数组已空了!
bool HpEmpty(Hp* php)//返回数组个数
{
assert(php);
return php->size == 0;
}
int HpGetSize(Hp* php)
{
assert(php);
return php->size;//返回个数
}
堆的销毁:
将数据置为0,释放数组开辟的空间,然后置空即可
void HpDestroy(Hp* php)
{
assert(php);
php->size = 0;
php->capacity = 0;
free(php->a);//释放开辟的空间
php->a = NULL;
}
堆的打印:
因为堆的实际存储是以数组的形式,所以我们只需遍历循环就行了
void HpPrint(Hp* php)
{
assert(php);
for (int i = 0; i < php->size; i++)//循环遍历
{
printf("%d ", php->a[i]);
}
printf("\n");
}
结语:
幸得识卿!