引言
上一篇文章中,介绍了二叉树的相关基础知识。
并且了解到二叉树的表示有两种结构:顺序结构与链式结构。即,使用顺序表或链表来表示二叉树:
戳我看树与二叉树详解哦
在这篇文章中就将介绍堆这种数据结构。并使用顺序结构来表示堆:
(需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两个定义:一个是数据结构,一个是操作系统中管理内存的一块区域分段)
堆
如果有一个数据的集合,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:根结点的值始小于(或大于)子结点的值。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆:
(需要注意的是,堆是一种完全二叉树。所以我们在表示堆时,用顺序表来存储会更方便)
在上面的是逻辑结构,下面的数组是顺序结构。每一个结点的数据都对应着数组中的一个位置,该位置对应着一个下标。
对于结点数据的下标与结点位置,有着一些规律:
父亲结点的下标 = (孩子结点下标-1)/2;
左孩子结点的下标 = 父亲结点的下标2+1;
右孩子结点的下标 = 父亲结点的下标2+2。
这些规律使我们在根据逻辑结构访问堆中的元素时,会很方便。
接下来就可以尝试实现堆的各种接口:
接口实现
与顺序表类似,在实现堆之前,我们需要创建一个结构体变量,用来存储数组、容量与数据个数:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* data;
int size;
int capacity;
}Heap;
堆的构建与销毁
构建堆时,我们需要为数据开辟出一块物理结构。我们可以动态开辟一块空间来存储数据;并且将元素个数初始化为0;将堆的容量初始化为10:
// 堆的构建
void HeapCreate(Heap* php, int n)
{
assert(php);
php->data = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->data == NULL)
{
perror("malloc");
return;
}
php->size = 0;
php->capacity = n;
}
销毁堆时,我们直接free释放动态开辟的数组,然后将结构体中的值全部初始化为0即可:
// 堆的销毁
void HeapDestory(Heap* php)
{
assert(php);
free(php->data);
php->capacity = 0;
php->size = 0;
}
在堆中插入数据
创建好存储数据的空间后,就可以在这个空间中存储数据了。
前面提到,堆有大堆与小堆之分。这次我们就先实现在大堆中插入数据:
对于数组而言,显然是尾插更加方便。当然,在插入数据之前,需要判断堆是否已满:若已满,则需要realloc扩容。然后将下标为size的元素赋值为x,size++即可;
但是在尾插一个数据后,我们需要调整新插入的数据的位置,保证堆依旧是一个大堆。为了实现这样的目的,我们需要一个函数,可以实现比较逻辑结构中父子结点的值:若子节点的值小于父亲结点的值,就将父子结点的值交换,直到再次形成一个大堆。(这个过程称为向上调整建堆)
由子节点访问父亲结点时,根据上面提到的数组中的下标的规律即可:
当然,为了交换方便,我们可以将交换的操作初始化为一个Swap函数:
//交换
void Swap(Heap* php, int a, int b)
{
assert(php);
int temp = php->data[a];
php->data[a] = php->data[b];
php->data[b] = temp;
}
//向上调整
void AdjustUp(Heap* php, int child)
{
assert(php);
int parent = (child - 1) / 2;
while (child > 0)
{
if (php->data[child] > php->data[parent])
{
Swap(php, child, parent);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(Heap* php, HPDataType x)//尾插,大者为爹,尾向上交换
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tempptr = (HPDataType*)realloc(php->data, sizeof(HPDataType) * php->capacity * 2);
if (tempptr == NULL)
{
perror("realloc");
return;
}
else
{
php->data = tempptr;
php->capacity *= 2;
}
}
php->data[php->size] = x;
php->size++;
AdjustUp(php, php->size - 1);
}
从堆中删除数据
我们发现,对于大堆而言,堆顶的元素永远是最大的那个元素,而下面的元素的大小顺序就不确定了;对于小堆而言堆顶的元素是最小的。所以我们在删除堆中数据时,删除堆顶的元素是有意义的。
在删除堆顶元素时,如果直接用后面的元素覆盖前面的元素,就会导致堆的结构完全乱序,调整起来就会很麻烦。这里提供一种先交换后调整的思路(对于大堆):
首先将堆顶的元素与堆尾的元素交换,然后size–,将原堆顶的元素删除。这样可以保证除堆顶元素外,其他位置的结构都是完好的;
然后从顶开始,比较父亲结点与较大的子结点的值,若父亲结点小于较大的子节点,则交换它们的值。直到再次形成大堆:(这个过程称为向下调整建堆)
由父亲节点访问子结点时,根据上面提到的数组中的下标的规律即可:
//向下调整
void AdjustDown(Heap* php, int parent)//交换爹与大儿子
{
assert(php);
int child = parent * 2 + 1;//默认大儿子为左儿子
while (child < HeapSize(php))
{
if (child + 1 < HeapSize(php) && php->data[child + 1] > php->data[child])
{
child += 1;
}
if (php->data[parent] < php->data[child])
{
Swap(php, parent, child);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆的删除
void HeapPop(Heap* php)//首尾交换,去尾,头向下交换
{
assert(php);
Swap(php, 0, php->size - 1);//交换
php->size--;//去尾
AdjustDown(php, 0);//由根开始向下调整
}
访问堆顶数据
访问堆顶数据时,即访问数组中的第一个元素:
// 取堆顶的数据
HPDataType HeapTop(Heap* php)
{
assert(php);
return php->data[0];
}
计算堆中数据个数
计算堆中数据个数时,即结构体变量中的size变量的值:
// 堆的数据个数
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
判断堆是否为空
判断堆是否为空时,当size为0时,返回真;否则返回假:
// 堆的判空:为空返回真
int HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
总结
到此,关于数据结构堆的介绍就结束了,也就是二叉树的顺序结构
在下一篇文章中将介绍二叉树的链式结构及一些题目详解,欢迎大家持续关注哦
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦