文章目录
前言
上篇了解树和二叉树相关的概念,这篇学习一种特殊的二叉树--堆,通过认识堆来实现堆和堆的应用。
一、堆的概念与结构
如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , ..., k n −1 } ,把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足: K i <= K 2∗ i +1 ( K i >= K 2∗ i +1 且 K i <= K 2∗ i+2 ), i = 0、1、2... ,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
小根堆:
大根堆:

堆的特点:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
从二叉树的性质讨论出:
对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从
0 开始编号,则对于序号为 i 的结点有:- 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,无双亲结点
- 若 2i+1<n ,左孩子序号: 2i+1 , 2i+1>=n 否则无左孩子
- 若 2i+2<n ,右孩子序号: 2i+2 , 2i+2>=n 否则无右孩子
二、堆的实现
堆的定义
由此,我们可知堆的底层是数组来实现的,则堆的结构是顺序结构,可写出堆的结构定义
cpp
typedef int HPDatatype;
//堆的结构
typedef struct Heap
{
HPDatatype* arr;
int size; //有效数据个数
int capacity; //容量
}HP;
1.初始化堆
代码解析:
先对未开辟空间的数组指针置为空,再对有效个数和容量大小都置为0.
cpp
//初始化
void HPIint(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
2.堆的销毁
代码解析:
先判断堆是否为空,不为空先对数组进行释放再置为NULL,再对有效个数和容量大小都置为0
注:对堆开辟空间使用完后就要对堆进行销毁,避免造成空间浪费,因此要对堆进行销毁
cpp
//销毁
void HPDesTroy(HP* php)
{
//判断堆是否为空,不为空就直接free再置空
assert(php);
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
3.堆的插入
代码解析:
堆的插入就是在尾部进行数据插入。先判断堆是否已满,堆已满就进行 realloc 增容并更新capacity。增容后,把插入进来的数据进行重新调整,用 AdjustUp 函数对堆进行调整
cpp
//往堆中插入数据(以建小堆为例)
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, sizeof(HPDatatype) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
//插入数据
php->arr[php->size] = x;
//插入数据后用向上调整方法来调整成小堆或者大堆
AdjustUp(php->arr, php->size);
++php->size;
}
3.1向上调整算法
接下来给大家介绍AdjustUp 函数
代码解析:
先将元素插入到堆的末尾,即最后一个孩子之后
插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可由二叉树的性质,我们可知已知孩子结点可求父结点:Parent =(child-1)/2。在这里我们建小堆,要求孩子结点大于父结点,如果不满足就对进行交换,在让child 走到parent ,parent 走到parent 的父结点;如果满足不用交换直接跳出循环。

//向上调整算法:
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;//如果子节点大于父节点,则不需要变化直接跳出循环
}
}
}
时间复杂度:
因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个结点不影响最终结果)
分析:
第1层, 2 0 个结点,需要向上移动0层
第2层, 2 1 个结点,需要向上移动1层
第3层, 2 2 个结点,需要向上移动2层
第4层, 2 3 个结点,需要向上移动3层
......
第h层, 2 h −1 个结点,需要向上移动h-1层
则需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)

4.堆的判空
代码解析:用bool函数来判断堆是否为空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
5.求有效个数
代码解析:直接返回有效个数即可
int HPSize(HP* php)
{
assert(php);
return php->size;
}
6.删除堆顶数据
代码解析:
先判断堆是否为空堆,是就返回0,不是返回有效数据;让根结点和最后一个元素进行交换,把交换后的最后一个元素删除后,再进行向下调整算法
//删除堆顶数据(以建小堆为例)
void HPPop(HP* php)
{
assert(!HPEmpty(php));
//交换根节点和最后一个节点,再对新的最后一个节点进行删除
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
//使用向下调整算法
AdjustDown(php->arr, 0, php->size);
}
6.1向下调整算法
代码解析:
将堆顶元素与堆中最后一个元素进行交换
删除堆中最后一个元素
将堆顶元素向下调整到满足堆特性为止由二叉树的性质,我们可知,已知parent 结点的下标,就可求左,右孩子结点的下标,左下标:2(parent )+1=child, 右下标:2parent +2=child 。把最后一个元素删除后,这里需要建小堆,先找到孩子结点中较小的结点,把父结点和较小的孩子结点进行交换,再让父结点走到较小的孩子结点的交换前的位置,再更新孩子结点的下标;如果父结点小于孩子结点就不用交换,直接跳出循环。

//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n)
{
//已知父节点,求子节点又可求左右子节点
int child = parent * 2 + 1;//左子节点
while (child<n)
{
//找到最小的节点,让其与父节点进行交换,谁小谁往上调(如果是大堆,谁大谁往上调)
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层, 2 0 个结点,需要向下移动h-1层
第2层, 2 1 个结点,需要向下移动h-2层
第3层, 2 2 个结点,需要向下移动h-3层
第4层, 2 3 个结点,需要向下移动h-4层
......
第h-1层, 2 h −2 个结点,需要向下移动1层
则需要移动结点总的移动步数为:每层结点个数 * 向下调整次数

注:堆的向上调整算法和向下调整算法都可以建大堆和小堆。向上调整算法主要用于堆插入,向下调整算法主要用于堆应用和堆排序。通过两者得出的时间复杂度,可知向下调整算法时间复杂度比向上调整算法复杂度好。
7.获取栈顶数据
代码解析:直接返回栈顶的数据
HPDatatype HPTop(HP* php)
{
assert(!HPEmpty(php));
return php->arr[0];
}
三、完整源码
Heap,h:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
//一、用顺序结构实现完全二叉树,底层是数组
typedef int HPDatatype;
//堆的结构
typedef struct Heap
{
HPDatatype* arr;
int size; //有效数据个数
int capacity; //容量
}HP;
//初始化
void HPIint(HP* php);
//销毁
void HPDesTroy(HP* php);
//往堆中插入数据
void HPPush(HP* php, HPDatatype x);
//删除堆顶数据
void HPPop(HP* php);
//判空
bool HPEmpty(HP* php);
//求size
int HPSize(HP* php);
//获取栈顶数据
HPDatatype HPTop(HP* php);
//向上调整算法:
void AdjustUp(HPDatatype* arr, int child);
//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n);
Heap,c:
#include"Heap.h"
//初始化
void HPIint(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
//销毁
void HPDesTroy(HP* php)
{
assert(php);
//判断堆是否为空,不为空就直接free再置空
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
//交换:父节点与子节点比较,谁小谁往上调(谁大谁往上调)
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
//向上调整算法:
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;//如果子节点大于父节点,则不需要变化直接跳出循环
}
}
}
//往堆中插入数据(以建小堆为例)
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, sizeof(HPDatatype) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
//插入数据
php->arr[php->size] = x;
//插入数据后用向上调整方法来调整成小堆或者大堆
AdjustUp(php->arr, php->size);
++php->size;
}
///
//对堆进行判断是否为空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//求size
int HPSize(HP* php)
{
assert(php);
return php->size;
}
//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n)
{
//已知父节点,求子节点又可求左右子节点
int child = parent * 2 + 1;//左子节点
while (child<n)
{
//找到最小的节点,让其与父节点进行交换,谁小谁往上调(如果是大堆,谁大谁往上调)
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;
}
}
}
//删除堆顶数据(以建小堆为例)
void HPPop(HP* php)
{
assert(!HPEmpty(php));
//交换根节点和最后一个节点,再对新的最后一个节点进行删除
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
//使用向下调整算法
AdjustDown(php->arr, 0, php->size);
}
//获取栈顶数据
HPDatatype HPTop(HP* php)
{
assert(!HPEmpty(php));
return php->arr[0];
}
test,c:
#include"Heap.h"
void test()
{
HP hp;
HPIint(&hp);
HPPush(&hp, 6);
HPPush(&hp, 4);
HPPush(&hp, 8);
HPPush(&hp, 10);
//HPPop(&hp);
//HPPop(&hp);
//HPPop(&hp);
//HPPop(&hp);
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPPop(&hp);//取栈顶就要删除栈顶,不然会一直循环
}
HPDesTroy(&hp);
}
总结
非常感谢大家阅读完这篇博客。希望这篇文章能够为您带来一些有价值的信息和启示。如果您发现有问题或者有建议,欢迎在评论区留言,我们一起交流学习。