系列文章目录
文章目录
前言
一、堆的概念及结构
1. 堆的概念
堆(Heap):一种完全二叉树,其每个节点都比其子节点的数值大或等于(小或等于)
- 小根堆:每个节点都比其子节点小或等于,根节点最小
- 大根堆:每个节点都比其子节点大或等于,根节点最大
系统堆:是用来划分系统内存区域的,与数据结构中的堆不同
2. 堆的结构定义
堆一般使用顺序结构存储
typedef int HPDataType;
typedef struct Heap
{
HPDataType* data; //数据域
int size; //有效元素个数
int capacity; //堆的容量
} Heap;
二、堆的数据读取
由于堆是由顺序结构实现的,所以访问数据时没有孩子指针或父指针指向该节点的父节点或子节点,所以我们就需要使用该节点的下标计算出父节点和子节点。
1. 父亲节点和子节点下标的关系
三、堆的创建
1.大小堆
堆的创建是将元素插入到堆的末尾或开头,再将插入的元素调整到堆中特定的位置,使之构建成重新成为一个堆,调整堆的算法有2种,分别是向上调整算法和向下调整算法。
(1)升序 – 建大堆
【思考】排升序,建小堆可以吗?-- 可以(但不推荐)。
⚪首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第二小的数,不断重复上述过程。
【时间复杂度】建 n 个数的堆时间复杂度是 O(N),所以上述操作时间复杂度为 O(N²),效率太低,关系变得杂乱,尤其是当数据量大的时候,效率就更低。同时堆的价值也没有被体现出来,这样不如用直接排序。
⚪【最佳方法】排升序,因为数字依次递增,需要找到最大的数字,得建大堆。
首先对 n 个数建大堆。将最大的数(堆顶)和最后一个数交换,把最大的数放到最后。前面 n-1 个数的堆结构没有被破坏(最后一个数不看作在堆里面的),根节点的左右子树依然是大堆,所以我们进行一次向下调整成大堆即可选出第 2 大的数,放到倒数第二个位置,然后重复上述步骤。
【时间复杂度】:建堆时间复杂度为 O(N),向下调整时间复杂度为 O(log₂N),这里我们最多进行N-2 次向下调整,所以堆排序时间复杂度为 O(N*log₂N),效率相较而言是很高的。
(2)降序 – 建小堆(与建大堆同理)
【最佳方法】排降序,因为数字越来越小,需要找到最小的数字,得建小堆。
首先对 n 个数建小堆。将最小的数(堆顶)和最后一个数交换,把最小的数放到最后。前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是小堆,所以我们进行一次向下调整成小堆即可选出第2小的数,放到倒数第二个位置,然后重复上述步骤。
【时间复杂度】:建堆时间复杂度为 O(N),向下调整时间复杂度为 O(log₂N),这里我们最多进行N-2 次向下调整,所以堆排序时间复杂度为O(N*log₂N),效率相较而言是很高的。
2. 向上调整算法
实现交换的函数
#define HPDataType int
void Swap(HPDataType* p1,HPDataType* p2) //实现交换
{
HPDataType tem=*p1;
*p1=*p2;
*p2=tem;
}
2.1 向上调整原理
将最后一个元素与父节点进行对比,若不满足堆条件,则进行交换,直到这个元素小于父节点或到达堆顶
-
- 最后一个元素和父节点对比,若是父节点大于该节点,则交换二者位置
-
- 一直对元素进行调整,直到父节点小于该节点或者堆的对比结束就跳出循环
-
- 判断堆结束的条件是 孩子节点等于或小于0,证明孩子节点已经到根节点
-
- 将小根堆的调整改为大根堆只需要将比较条件改为父节点小于子节点交换
2.2 向上调整代码实现
void AdjustUp(HPDataType* data, int child)
{
int parent = 0;
while (child > 0) //条件
{
parent = (child - 1) / 2; //父节点和子节点的关系
if (data[child] < data[parent]) //父节点<子节点 交换
{
Swap(&data[child], &data[parent]);
child = parent; //下一个
}
else
{
break;
}
}
}
3. 向下调整算法
3.1 向下调整原理
将第一个元素与最小(或最大)的子节点进行对比,若不满足堆条件,则进行交换,直到这个元素小于(或大于)父节点或到达堆的底部
-
- 将堆顶元素与其最小的子节点进行对比,若该元素小于最小的子节点,则与该节点交换
-
- 先获取左孩子,再与右孩子进行对比,若是右孩子小,则最小孩子的下标加1,得到右孩子的下标(先假设左边孩子最小——假设法)
-
- 与右孩子的下标对比之前,必须先判断右孩子是否存在(条件容易漏)
-
- 循环结束的条件是孩子节点大于等于堆长度,此时调整范围以及超过堆范围
-
- 将小根堆的调整改为大根堆只需要将比较条件改为父节点小于子节点交换,同时别忘了找最大子节点的比较也要改
3.2 向下调整代码实现
// 数组 数组数据个数 从哪个开始向下调整
void AdjustDown(HPDataType* data, int size, int parent)
{
//先找最小孩子
//假设法——左孩子最小
int minchild = parent * 2 + 1;
while (minchild < size)
{
//右孩子要存在,右孩子<左孩子
if (minchild + 1 < size && data[minchild+1] < data[minchild ])
{
minchild++; //最小孩子下标更新
}
if (data[minchild] < data[parent]) //最小孩子<父亲
{
Swap(&data[minchild], &data[parent]); //交换
parent = minchild; //下一个
minchild = parent * 2 + 1;
}
else
{
break;
}
}
}
四、堆的实现
1. 初始化
- 这里传入二级指针,先为Heap分配空间,扩容时再为data域分配空间
- 初始化时将各成员变量赋初值
void HeapInit(Heap** pphp)
{
assert(pphp != NULL);
*pphp = (Heap*)malloc(sizeof(Heap));
if (*pphp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
(*pphp)->data = NULL;
(*pphp)->capacity = (*pphp)->size = 0;
}
2. 插入数据
- 先判断是否堆满,若是满了,就扩容
- 插入数据,就是尾插数值
- 调用向上调整函数,将最后一个数值调整成堆
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
//扩容
if (php->size == php->capacity)
{
//三目运算符(为0时赋值4,不然下面它倍数恒为0)
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* temp = (HPDataType*)realloc(php->data, newcapacity * sizeof(HPDataType));
if (temp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
php->data = temp;
php->capacity = newcapacity;
}
//插入数据
php->data[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->data, php->size - 1);
}
3. 删除堆顶元素
- 删除的是堆顶元素
- 不能使用后面的元素依次覆盖前一个元素,否则浪费时间,还会破坏堆的结构
- 将堆顶元素和最后一个元素交换,再size减一,就完成了堆顶元素的删除
- 调用向下调整函数,将堆顶元素重构成堆结构
void HeapPop(Heap* php)
{
assert(php != NULL);
if (php->size == 0) //数据个数不为0
{
return;
}
Swap(&(php->data[0]), &(php->data[php->size-1])); //堆顶元素和最后一个元素交换
php->size--; //数据元素减1
AdjustDown(php->data, php->size, 0); //向下调整
}
4. 获取堆顶元素
- 先判空,若为空,则中断程序
- 返回第一个元素值
HPDataType HeapTop(Heap* php)
{
assert(php != NULL);
assert(php->size != 0);
return php->data[0];
}
5. 获取元素个数
返回size的值即可
size_t HeapSize(Heap* php)
{
assert(php != NULL);
return (size_t)php->size;
}
6. 判断堆为空
元素个数为0时,堆为空
_Bool HeapEmpty(Heap* php)
{
assert(php != NULL);
return php->size == 0;
}
7. 销毁堆
- 使用二级指针传入堆,先释放data,再释放堆
- 最后将堆指针指向空
void HeapDestroy(Heap** pphp)
{
assert(pphp != NULL);
free((*pphp)->data);
free(*pphp);
*pphp = NULL;
}
五、堆的创建的时间复杂度
【拓展】采用向上调整法时
如果堆的创建过程使用向上调整算法,那么每次插入一个新元素时都需要进行一次向上调整操作,以确保新插入的元素能够满足堆的性质。
【时间复杂度】
向上调整法:节点数量越多,调整次数越多。
向下调整时:
第1层 ——向下调整h-1次 —— 2^0个节点
第2层 —— 向下调整h-2次 —— 2^1个节点
第3层 —— 向下调整h-3次 —— 2^2个节点
————————————
第h-3层 —— 向下调整3次 —— 2^(h-4)个节点
第h-2层 ——向下调整2次 —— 2^(h-3)个节点
第h-1层 ——向下调整1次—— 2^(h-2)个节点
属于:等差✖等比(错位相乘法)
(h-1)*2^0 +(h-2)*2^1+ …(1) 2^(h-2)
向下调整法:节点数量越多,调整次数越少。
结论**: 使用向上调整算法创建堆需要进行多次调整操作,而使用向下调整算法只需要进行一次调整操作。因此,从实际操作的角度来看,使用向下调整算法创建堆更为高效。同时,向下调整算法也更为直观,容易理解和实现。因此,在实际应用中,一般会选择使用向下调整算法来创建堆。
六、堆的应用
1. 堆的应用
- 堆排序:[点击查看详情]
- Top K问题:[[点击查看详情]]