目录
一、堆的概念与结构
如果有一个集合K={k0,k1,k2,…………ki-1,ki-1},将它的所有元素的值按照完全二叉树的存储形式存储到一个一维数组中:并且满足ki>=ki2+1,ki>=ki2+2(ki<=ki2+1,ki<=ki2+2),那么我们称这个数组为大堆(小堆),将根节点最大的堆称为最大堆或大根堆,将根节点最小的堆称为最小堆或小根堆
由此可见
- 大堆的父节点的值总是大于或等于子节点的值
- 小堆则相反
堆具有以下性质
- 对于具有
n
个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从0开始编号,则对于序号为i
的结点有:1.若
i>0
i位置结点的双亲序号:(i-1)/2
;i=0,i为根结点编号,⽆双亲结点
2.若2i+1<n
,左孩⼦序号:2i+1
,2i+1>=n
否则⽆左孩⼦
3.若2i+2<n
,右孩⼦序号:2i+2
,2i+2>=n
否则⽆右孩⼦
- 堆中的某个结点的值总是不大于或不小于其父节点的值
- 堆是一棵完全二叉树
二、堆的实现
为了方便后续操作和代码维护
这里采用多文件结构
注意:heap.h应该在头文件一栏,小编当时直接在源文件创建了,请勿模仿
Heap.h
包含头文件,堆的结构体的定义,函数声明Heap.c
包含堆的函数功能的基本实现Test.c
包含堆的函数基本测试
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//堆的结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size; //有效数据个数
int capacity;//空间大小
}HP;
void Swap(int* x, int* y);
void AdjustUp(HPDataType* arr, int child);
void AdjustDown(HPDataType* arr, int parent, int n);
void HPInit(HP* php);
void HPDestroy(HP* php);
void HPPrint(HP* php);
void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
//取堆顶的数据
HPDataType HPTop(HP* php);
//判空
bool HPEmpty(HP* php);
下面就各部分详细讲解:
堆的结构定义
堆的实现是利用数组,利用typedef
对堆的结构体和里面元素类型重命名,便于后续复习理解;除此之外,需要用size和capacity分别记录有效数据个数和堆的空间大小。
本文默认创建大堆。
//堆的结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size; //有效数据个数
int capacity;//空间大小
}HP;
堆的初始化
void HPInit(HP* php)
{
php->arr = NULL;
php->size = php->capacity = 0;
}
初始情况下,将堆的arr置为NULL,将size和capacity置为0后面插入数据时再增容,当然,也可以在初始化时就分配一定空间,代码如下
void HeapInit2(HP* php)
{
php->arr = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc error");
return;
}
php->capacity = 4;
php->size = 0;
}
后面代码都是在第一种写法上的(影响不大)完成的
堆的销毁
这应该是程序最后的代码,此时释放php->arr
,然后将其置为空,防止野指针,最后将堆的size和capacity置为0。
void HPDestroy(HP* php)
{
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
堆的打印
很“朴实”的打印方法,但要注意,打印出的堆肯定是个数组不是树状图,而且 ,即使我们想创建大堆,其结果肯定不是升序。
void HPPrint(HP* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->arr[i]);
}
printf("\n");
}
堆的插入与向上调整法
- 1.在堆的插入中,我们只能在最后的孩子结点插入数据,不能在父节点插入
- 2.为了确保堆的性质(在这个例子中是大堆性质,即每个节点的值都大于或等于其子节点的值)仍然保持不变。向上调整算法会从插入新元素的位置开始,不断将该元素与其父节点比较,如果它比父节点大,则交换它们的位置,直到满足堆的性质或者到达堆的根节点。
- 3.计算父节点索引:首先根据子节点的索引 child 计算其父节点的索引 parent,在完全二叉树中,对于索引为 i 的节点,其父节点的索引为 (i - 1) / 2。
- 4.循环比较并调整:使用 while 循环,只要 child 大于 0,就继续进行比较和调整操作。
- 5.比较子节点和父节点的值:如果子节点的值大于父节点的值,说明不满足大堆的性质,需要进行调整。
- 6.交换子节点和父节点的值:调用 Swap 函数交换子节点和父节点的值。
- 7.将子节点的索引更新为原来父节点的索引,然后重新计算新的父节点的索引。
- 8.继续比较新的子节点和父节点的值,直到满足堆的性质或者到达堆的根节点。
- 9.:如果子节点的值不大于父节点的值,说明已经满足堆的性质,退出循环。
void AdjustUp(HPDataType* arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//大堆:>
//小堆:<
if (arr[child] > arr[parent])
{
//调整
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
这部分将Swap(交换)单独封装成一个函数,因为后面还会用到
void Swap(HPDataType* x, HPDataType* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
好了,下面正式写堆的插入:
1.因为之前并没有增容,并且每次调用我们不确定是否有最够的容量插入,因此先要扩容
2.扩容操作
- 定义新的容量 newcapacity,若之前为空,初始容量为 4,之后每次扩容为原来的 2 倍。
- 运用 realloc 函数重新分配内存,把堆数组的容量扩大。
- 检查内存分配是否成功,若失败,输出错误信息并退出程序。
- 更新堆数组指针 php->arr 和堆的容量 php->capacity。
3.插入新元素:把新元素 x 插入到堆数组的末尾 php->arr[php->size]。
4.堆调整:调用 AdjustUp 函数对堆进行向上调整,保证堆的性质(大堆或者小堆)维持不变。
5.更新元素数量:将堆的元素数量 php->size 加 1。
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);
++php->size;
}
判空
如果有效个数size与0相等则为空
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
堆的向下调整与出堆
AdjustDown
函数实现了堆的向下调整算法,用于在堆的根节点(或某个非叶子节点)的值被修改后,重新调整堆结构,使其满足大堆(每个节点的值都大于或等于其子节点的值)的性质。
1.初始化孩子节点索引:根据父节点的索引 parent 计算其左孩子节点的索引 child。
2.循环调整:只要 child 小于数组长度 n,就继续进行调整操作。
- 选择较大的孩子节点:如果右孩子节点存在且其值大于左孩子节点的值,则将 child 更新为右孩子节点的索引。
- 比较并交换:如果较大的孩子节点的值大于父节点的值,则交换它们的值,并更新 parent 和 child 的索引,继续向下调整。
- 退出循环:如果孩子节点的值不大于父节点的值,说明已经满足堆的性质,退出循环。
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;//左孩子
while (child > 0)
{
//保障右孩子
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
if (arr[child] > arr[parent])
{
//调整
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
明白向下调整后我们来写出堆操作
1.检查堆是否为空:使用 assert 宏确保堆不为空。
2.交换堆顶和最后一个元素:将堆顶元素和堆的最后一个元素交换位置。
3.减少堆的元素数量:将堆的元素数量 php->size 减 1,相当于移除了原来的堆顶元素。
4.向下调整堆:调用 AdjustDown 函数从堆顶(索引为 0)开始进行向下调整,使堆重新满足大堆的性质。
void HPPop(HP* php)
{
assert(!HPEmpty(php));
// 0 php->size-1
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
//向下调整
AdjustDown(php->arr, 0, php->size);
}
取堆顶数据
堆顶数据的值在数组中即为arr[0]
//取栈顶的数据
HPDataType HPTop(HP* php)
{
assert(!HPEmpty(php));
return php->arr[0];
}
堆排序
之前说了,大堆对应数组并不是降序,同样,小堆也不是升序,那么我们该怎么排序呢?
方法一既然是数组,那我们是否可以直接冒泡排序呢?
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
似乎可行,但,这和堆有什么关系~~
方法二借助数据结构堆实现
好,我们借助本篇学到的东西排序
- 建立一个堆
- 初始化
- 在堆中插入n个元素
HPPush
- 排序:我们知道一个堆或者完全二叉树根节点总是最值,因此,每次取堆顶数据
HPTop
,将该元素赋值给arr,再让该堆出堆HPPop
,可以去掉最值,然后重复该操作直到该堆为空HPEmpty
- 销毁堆
void HeapSort1(int* arr, int n)
{
HP hp; //——————借助数据结构堆来实现堆排序
HPInit(&hp);
for (int i = 0; i < n; i++)
{
HPPush(&hp, arr[i]);
}
int i = 0;
while (!HPEmpty(&hp))
{
int top = HPTop(&hp);
arr[i++] = top;
HPPop(&hp);
}
HPDestroy(&hp);
}
But,你是否和我一样感觉十分麻烦,因为我们如果要完成堆排序,还要创建一个堆,完成插入、取顶等操作,如果是一个考试题,我们不可能为了堆排序实现一整个堆的操作
方法三利用堆的思想
这才是真正的堆排序
堆排序是一种高效的排序算法,其时间复杂度为 (O(n log n))。
整体思想先构建一个最大堆,接着重复将堆顶元素(最大值)与堆的最后一个元素交换,并对剩余元素重新调整为最大堆,直到整个数组有序。
1.建堆,以大堆为例
升序——建大堆
降序——建小堆
- 起始位置计算:(n - 1 - 1) / 2 计算出最后一个非叶子节点的索引。在完全二叉树中,最后一个非叶子节点的索引为 (n - 2) / 2(n 为数组长度),因为叶子节点不需要进行向下调整操作。
- 向下调整:从最后一个非叶子节点开始,依次向前对每个非叶子节点调用
AdjustDown
函数进行向下调整,使得每个节点的值都大于或等于其子节点的值,最终构建出一个最大堆。
2.堆排序
- 交换堆顶和最后一个元素:
Swap(&arr[0], &arr[end])
将堆顶元素(最大值)与堆的最后一个元素交换,这样最大值就被放到了数组的末尾。 - 重新调整堆:
AdjustDown(arr, 0, end)
对交换后的堆(除了已经排好序的最后一个元素)进行向下调整,使得剩余元素重新构成一个最大堆。 - 缩小堆的范围:end-- 缩小堆的范围,排除已经排好序的元素,继续进行下一轮的交换和调整操作,直到整个数组有序。
void HeapSort(int* arr, int n)
{
//建堆——向下调整算法建堆
for (int i = (n-1-1)/2; i>=0; i--)
{
AdjustDown(arr, i, n);
}
//堆排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
小堆同样的原理