文章目录
- 写在前面
- 1. 堆的概念和性质
- 1.1 堆的概念
- 1.2 堆的性质
- 2 堆的实现
- 2.1 堆结构的定义
- 2.2 堆的初始化
- 2.3 堆的插入
- 2.3.1 向上调整算法
- 2.3.2 堆的插入元素过程
- 2.4 堆的删除
- 2.4.1 向下调整算法
- 2.4.2 堆的删除元素过程
- 2.5 获取堆顶元素
- 2.6 获取堆元素个数
- 2.7 判断堆是否为空
- 2.8 堆的销毁
写在前面
本片文章使用C语言实现了一种非线性的数据结构——堆(小根堆)。
1. 堆的概念和性质
1.1 堆的概念
如果有一个关键码的集合K = {K0 ,k1 ,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:ki <= k2*i+1且 ki <= k2*i+2(ki >= k2*i+1且 ki >= k~2*i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
画个图理解一下:
1.2 堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
2 堆的实现
由于堆的逻辑结构是一颗完全二叉树,所以在实际存储时,通常使用数组来表示这颗完全二叉树。
数组表示法的好处:
- 由于完全二叉树的性质,数组表示法可以将元素按照层级顺序依次存储,不会有额外的空间浪费,使得存储更为紧凑。
- 数组的索引关系可以方便地表示父节点和子节点之间的关系。对于数组中的第 i 个元素,其左子节点在 2i+1 的位置,右子节点在2i+2 的位置,而父节点在 (i−1) / 2的位置。
画个图理解一下:
这种数组表示法使得堆的操作更加高效,因为可以通过索引直接访问父子节点,而无需使用指针。
2.1 堆结构的定义
从上面的分析可以知道堆结构的定义通常包括一个动态开辟的数组,用来存储数据。为了方便扩容,增加一个用于记录堆中元素个数以及一个用于记录堆容量的变量。
下面是堆结构的定义:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* nums;//动态开辟的数组
int size;//堆中元素个数
int capacity;//堆容量
}Heap;
2.2 堆的初始化
在初始时,堆是一个空堆,因此将堆的元素数组指针 nums 初始化为 NULL,表示还没有分配存储空间。此时 capacity 为 0,表示堆的容量为 0,还没有存储任何元素。size 也为 0,表示当前堆中的元素个数为 0。
这种初始化状态是堆数据结构的起始状态,之后可以根据需要动态分配存储空间,并通过插入元素来逐渐构建堆的结构。
代码如下:
void HeapInit(Heap* php)
{
assert(php);//检查参数有效性
php->nums = NULL;//初始化为 NULL,表示还没有分配存储空间
php->size = php->capacity = 0;
}
2.3 堆的插入
2.3.1 向上调整算法
在实现堆的插入之前需要介绍一下向上调整算法。这是因为向上调整算法是在堆的插入操作中使用的关键步骤。这算法确保在插入新元素后,仍然保持堆的性质。
以下是向上调整算法的一般步骤:
- 从插入的节点开始,向上比较与父节点的大小。
- 如果插入节点的值大于(小于)其父节点的值,交换这两个节点。
- 重复步骤 2 直到满足堆的性质或达到堆的根节点。
如向该小根堆:{ 15,18,19,25,28,34,65,49,27,37 }插入一个10,并用向上调整算法使该小根堆继续保持小根堆的性质:
画个图理解一下:
代码如下:
//交换两个数
void Swap(HPDataType* x1, HPDataType* x2)
{
HPDataType tmp = *x1;
*x1 = *x2;
*x2 = tmp;
}
//向上调整算法
void AdjustUp(int* nums, int child)
{
while (child > 0)
{
int parent = (child - 1) / 2;
//1.小堆 向上调整,孩子小于父亲就交换
//2.如果满足堆的性质,则不需要调整
//若实现大根堆,这里nums[child] < nums[parent]的 < 换成 >
if (nums[child] < nums[parent])
{
Swap(&nums[child], &nums[parent]);
}
else
{
break;
}
child = parent;
}
}
需要注意的是:堆的向上调整算法的前提是,插入位置之前的元素已经构成了一个有效的堆。这确保了插入新元素后,通过向上调整以后,整个堆仍然是一个有效的小顶堆。
2.3.2 堆的插入元素过程
在上面向上调整算法的介绍中,我们发现向堆中插入元素时,只需把新元素追加到堆的末尾,然后通过向上调整算法,使整个堆仍然是一个有效的堆。
堆的插入元素过程通常包括以下步骤:
- 将新元素追加到堆的末尾。在堆的实现过程中,我们是使用数组来表示堆的,而在数组表示的堆中,堆的末尾就是数组的最后一个位置,因此新元素被添加到数组的最后一个位置。
- 进行向上调整。 新元素与其父节点进行比较,如果新元素的值小于其父节点的值,它们交换位置。这个过程一直重复,直到满足堆的性质,即新插入的元素不再小于其父节点或者到达堆顶。
分析:
3. 当堆为空时,插入第一个元素时,它天然满足堆的性质,因此,无需进行向上调整。
4. 当堆不为空时,插入元素时就有可能会进行向上调整。这是因为插入的新元素被追加到堆的末尾,与其父节点进行比较。如果新元素的值小于其父节点的值,就会交换它们的位置。这个过程重复进行,直到新插入的元素不再小于其父节点或者到达堆顶。
画个图理解一下:
代码如下:
void HeapPush(Heap* php, HPDataType x)
{
assert(php);//检查参数有效性
//检查是否需要扩容
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->nums, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("HeapPush->realloc");
exit(-1);
}
php->nums = tmp;
php->capacity = newcapacity;
}
//插入新元素
php->nums[php->size++] = x;
//向上调整
AdjustUp(php->nums, php->size - 1);
}
2.4 堆的删除
2.4.1 向下调整算法
在实现堆的删除操作之前需要介绍一下向下调整算法。这是因为向下调整算法是在堆的删除操作中使用的关键步骤。向下调整算法通常在删除堆顶元素后使用,确保新的堆顶元素能够找到适当的位置以维持堆的有序性。
确保向下调整算法有效的前提:
堆的左右子树都满足堆的性质。这是因为向下调整算法的目的是将当前节点与其左右孩子中较小(或较大,具体取决于是小根堆还是大根堆)的值交换,以恢复堆的有序性。
如果堆的左右子树不满足堆的性质,那么通过向下调整算法进行交换可能无法保证整个堆的有序性。因此,在使用向下调整算法时,必须确保该节点的左右子树都是堆。
向下调整算法具体步骤如下:
-
找到较小的孩子: 如果存在左右孩子,找到值较小的孩子。如果当前节点有左孩子(位置为 2 * parent + 1),如果有右孩子(位置为 2 * parent + 2)。
-
比较与较小孩子的值: 将当前节点与较小孩子的值比较。
-
交换或结束: 如果当前节点的值大于较小孩子的值,则交换它们,并继续向下调整。如果当前节点的值小于等于较小孩子的值,则调整结束。
-
循环: 重复执行上述步骤,直到当前节点不再有孩子或当前节点的值小于等于孩子的值。
画个图理解一下:
代码如下:
//交换两个数
void Swap(HPDataType* x1, HPDataType* x2)
{
HPDataType tmp = *x1;
*x1 = *x2;
*x2 = tmp;
}
void AdjustDown(int* nums, int n, int parent)
{
// 左孩子的索引
int child = parent * 2 + 1;
// 循环直到没有左孩子
while (child < n)
{
// 如果右孩子存在且比左孩子小,选择右孩子
//若实现大根堆,这里nums[child + 1] < nums[child]的 < 换成 >
if (child + 1 < n && nums[child + 1] < nums[child])
{
++child;
}
// 如果孩子比父亲小,交换它们的值
//若实现大根堆,这里nums[child] < nums[parent]的 < 换成 >
if (nums[child] < nums[parent])
{
// 孩子比父亲大,堆的有序性已经恢复,退出循环
Swap(&nums[child], &nums[parent]);
}
else
{
break;
}
// 更新父亲和孩子的索引
parent = child;
child = parent * 2 + 1;
}
}
2.4.2 堆的删除元素过程
堆的删除元素过程通常包括以下步骤:
- 将堆顶元素与最后一个元素交换:为了方便删除堆顶元素,将堆顶元素与最后一个元素交换位置。
- 删除堆顶元素:将堆顶元素删除,此时堆的大小减一。
- 向下调整堆:由于删除了堆顶元素,需要通过向下调整算法,保持堆的有序性。向下调整的目的是将交换后的堆顶元素放置到正确的位置,使得堆仍然满足堆的性质。
画个图理解一下:
代码如下:
void HeapPop(Heap* php)
{
assert(php);//检查参数有效性
assert(!HeapEmpty(php));//检查堆是否为空
Swap(&php->nums[0], &php->nums[php->size - 1]);
php->size--;
AdjustDown(php->nums, php->size - 1, 0);
}
2.5 获取堆顶元素
获取堆顶元素步骤:
- 堆不为空时,才能取堆顶数据。
- 获取堆顶元素的操作是直接访问堆数组的第一个元素,因为在堆的表示中,堆顶元素总是存储在数组的第一个位置。
代码如下:
HPDataType HeapTop(Heap* php)
{
assert(php);//检查参数有效性
assert(!HeapEmpty(php));//检查堆是否为空
return php->nums[0];
}
2.6 获取堆元素个数
获取堆元素个数的操作可以直接返回堆的成员变量 size,该变量记录了堆中当前元素的个数。
代码如下:
int HeapSize(Heap* php)
{
assert(php);//检查参数有效性
return php->size;
}
2.7 判断堆是否为空
判断堆是否为空的操作可以通过检查堆的成员变量 size 是否为零来实现。如果 size 为零,表示堆中没有元素,即堆为空;否则,堆非空。
代码如下:
bool HeapEmpty(Heap* php)
{
assert(php);//检查参数有效性
return php->size == 0;
}
2.8 堆的销毁
在销毁堆的操作中,我们首先使用 free 函数释放堆中的元素数组所占用的内存,然后将数组指针设置为 NULL,并将堆的大小和容量都设置为0,表示堆已被销毁。这样的操作确保了在释放内存的同时将堆的状态清空,防止野指针和内存泄漏的问题。
代码如下:
void HeapDestroy(Heap* php)
{
assert(php);//检查参数有效性
free(php->nums);
php->nums = NULL;
php->size = php->capacity = 0;
}
至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!