【数据结构】——6.2 堆
目录
一、堆的概念及结构
1. 堆的概念
堆(Heap):一种完全二叉树,其每个节点都比其子节点的数值大或等于(小或等于)
- 小根堆:每个节点都比其子节点小或等于,根节点最小
- 大根堆:每个节点都比其子节点大或等于,根节点最大
系统堆:是用来划分系统内存区域的,与数据结构中的堆不同
2. 堆的结构定义
堆一般使用顺序结构存储
typedef int HPDataType;
typedef struct Heap
{
HPDataType* data; //数据域
int size; //有效元素个数
int capacity; //堆的容量
} Heap;
二、堆的数据读取
由于堆是由顺序结构实现的,所以访问数据时没有孩子指针或父指针指向该节点的父节点或子节点,所以我们就需要使用该节点的下标计算出父节点和子节点。
1. 子节点的读取
从以上该图中,我们发现节点的左子节点下标总是父节点下标的2倍加1,右子节点是左子节点的下标加1,所以得出两个公式:
左 子 节 点 : l e f t = p a r e n t × 2 + 1 左子节点:left=parent\times2+1 左子节点:left=parent×2+1
右 子 节 点 : r i g h t = p a r e n t × 2 + 2 右子节点:right=parent\times2+2 右子节点:right=parent×2+2
2. 父节点的读取
根据以上公式,不难推到出子节点计算公式为: p a r e n t = ( l e f t − 1 ) / 2 = ( r i g h t − 2 ) / 1 parent = (left-1)/2=(right-2)/1 parent=(left−1)/2=(right−2)/1
由上图得到left下标一定是奇数,right下标一定是偶数,则 奇 数 / 2 奇数/2 奇数/2在整数运算中等于 ( 奇 数 − 1 ) / 2 (奇数-1)/2 (奇数−1)/2,所以 ( l e f t − 1 ) / 2 = ( l e f t − 2 ) / 1 (left-1)/2=(left-2)/1 (left−1)/2=(left−2)/1,所以无论是左子节点还是右子节点,该节点下标减1后再除以2一定是父节点下标,所以公式为:
父 节 点 : p a r e n t = ( c h i l d − 1 ) / 2 父节点:parent=(child-1)/2 父节点:parent=(child−1)/2
三、堆的创建
堆的创建是将元素插入到堆的末尾或开头,再将插入的元素调整到堆中特定的位置,使之构建成重新成为一个堆,调整堆的算法有2种,分别是向上调整算法和向下调整算法。
1. 向上调整算法
1.1 向上调整原理
将最后一个元素与父节点进行对比,若不满足堆条件,则进行交换,直到这个元素小于父节点或到达堆顶
- 最后一个元素和父节点对比,若是父节点大于该节点,则交换二者位置
- 一直对元素进行调整,直到父节点小于该节点或者堆的对比结束就跳出循环
- 判断堆结束的条件是 孩子节点等于或小于0,证明孩子节点已经到根节点
- 将小根堆的调整改为大根堆只需要将比较条件改为父节点小于子节点交换
1.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;
}
}
}
2. 向下调整算法
2.1 向下调整原理
将第一个元素与最小(或最大)的子节点进行对比,若不满足堆条件,则进行交换,直到这个元素小于(或大于)父节点或到达堆的底部
- 将堆顶元素与其最小的子节点进行对比,若该元素小于最小的子节点,则与该节点交换
- 先获取左孩子,再与右孩子进行对比,若是右孩子小,则最小孩子的下标加1,得到右孩子的下标
- 与右孩子的下标对比之前,必须先判断右孩子是否存在
- 循环结束的条件是孩子节点大于等于堆长度,此时调整范围以及超过堆范围
- 将小根堆的调整改为大根堆只需要将比较条件改为父节点小于子节点交换,同时别忘了找最大子节点的比较也要改
2.2 向下调整代码实现
void AdjustDown(HPDataType* data, int size, int parent)
{
int minchild = parent * 2 + 1;
while (minchild < size)
{
//左孩子存在,右孩子未必存在,所以要判断一下child+1
if (minchild + 1 < size && data[minchild] > data[minchild + 1])
{
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 != NULL);
//扩容
if (php->size == php->capacity)
{
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)
{
return;
}
php->size--;
Swap(&(php->data[0]), &(php->data[php->size]));
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. 堆的应用
2. 完整代码
代码保存在gitee中:点击完整代码参考