片头
嗨! 小伙伴们,上一篇中,我们学习了队列相关知识,今天我们来学习堆这种数据结构,准备好了吗? 我们开始咯 !
一、堆
1.1 堆的概念
堆(Heap)是一种特殊的树,如果将一个集合中的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足一定的规则,则称之堆。堆的性质有:
- 堆中任意节点的值总是不大于或不小于其父节点的值
- 堆总是一颗完全二叉树
【补充】:满二叉树每一层都是满的;完全二叉树最后一层可以不满,但是从左到右必须是连续的
接下来引入大小堆的概念,这也是堆在建立之时必须遵守的规则,如果不满足其中任意一种,那么就不能称为堆。
大堆(大根堆/最大堆):树中任何一个父节点都大于或等于子节点,根节点是最大值。
小堆(小根堆/小根堆):树中任何一个父节点都小于或等于子节点,根节点是最小值。
1.2 堆的存储
因为堆是一种特殊的二叉树,其存储方式与完全二叉树的顺序存储方式相同。
顺序结构的存储就是使用数组来存储,一般只有完全二叉树适合用数组来存储,因为非完全二叉树的元素不连续会造成空间的浪费。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
使用数组来存储,父子节点的关系如下:
父节点:(子节点-1)/ 2
左孩子:(父节点*2)+ 1
右孩子:(父节点*2)+ 2
1.3 堆的应用
1. 堆排序
2. TopK问题
3. 优先级队列
二、堆的实现
2.1 堆的调整算法
假设给出一个数组,我们在逻辑上可以将其看作一颗完全二叉树,但是这个数组不能被称为堆。通过使用堆的调整算法我们可以将其调整成一个大/小堆。
(1)向下调整算法
向下调整算法就是将目标结点与左右子节点对比,符合条件则交换。
向下调整算法有一个前提:左右子树必须是堆
例如图中,以27为根的左右子树都满足小堆的性质,只有根节点不满足,所以只需要将其与左右子节点中较小的交换即可。
(2)向上调整算法
向上调整算法就是将目标节点与父节点对比,符合条件则交换
堆的插入就需要向上调整算法,例如我们在一个小堆中插入了一个新的元素:
使用向上调整算法:
2.2 堆的创建
堆的创建是堆排序中的一个重要部分。如果要将数组构建成堆,使用向下调整算法是最优解。但是根节点的左右子树都不是堆,所以我们从最后一个结点的父节点开始进行向下调整。
因为单个节点也能成堆,所以最后一层的所有叶子节点都可以被视为堆,接着我们就对数组进行从后向前遍历,从最后一个节点的父节点开始向下调整。
比如这个数组,我们将其构建成小堆,先将其看作一颗完全二叉树
然后从最后一个节点的父节点开始向下调整,因为要遵循小堆规则所以二者交换
交换完毕,遍历到前一个节点,此时父节点小于两个子节点,所以不需要交换,跳到10
此时父节点是10,左孩子节点是7,右孩子节点是3,3比7更小,所以10和3交换
现在,小堆就建立完毕啦!
2.3 堆的删除
一般情况下,堆的删除是指删除堆顶的数据。但是我们不能直接将数组的元素向前挪动覆盖第一个元素,因为在逻辑结构上,不同节点之间的关系已经建立,如果单纯的进行元素挪动就会破坏所有的关系,将整个堆破坏。
所以我们要先将堆顶的数据和最后一个数据交换,保持中间所有元素在堆中的相对位置不变,然后删除数组中的最后一个元素,再进行向下调整。
三、堆的代码实现
以小堆的创建为例,我们先创建一个头文件“Heap.h”和两个源文件"Heap.c"和"Test.c"
下面是"Heap.h"的代码:
#pragma once //防止头文件被二次引用
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int ElemType;
typedef struct Heap {
ElemType* arr;//动态数组(堆)
int size; //数组(堆)的大小
int capacity; //数组(堆)的容量
}Heap;
void HeapCreate(Heap* hp);//堆的初始化
void HeapDestroy(Heap* hp);//堆的销毁
void AdjustUp(ElemType* arr, int child);//向上调整(小堆)
void AdjustDown(ElemType* arr, int size, int parent);//向下调整(小堆)
void HeapPush(Heap* hp, ElemType x);//堆的插入
void HeapPop(Heap* hp);//堆的删除
ElemType HeapTop(Heap* hp);//取堆顶的数据
int HeapSize(Heap* hp);//堆的有效数据个数
int HeapEmpty(Heap* hp);//堆的判空
下面是"Heap.c"的代码:
#include"Heap.h"
//堆的初始化
void HeapCreate(Heap* hp) {
assert(hp); //断言,防止传入空指针
hp->arr = NULL; //动态数组为空
hp->capacity = 0;//初始时,数组的容量为0
hp->size = 0; //初始时,数组的大小为0
}
//堆的销毁
void HeapDestroy(Heap* hp) {
if (hp->arr) {
free(hp->arr);//将arr申请的空间释放
hp->arr = NULL;//置空
}
hp->capacity = 0;//堆的容量为0
hp->size = 0;//堆的大小为0
}
void Swap(int* a, int* b) //2个数交换
{
int temp = *a;
*a = *b;
*b = temp;
}
//向上调整(小堆)
void AdjustUp(ElemType* arr, int child) {
assert(arr); //断言,防止传入空指针
int parent = (child - 1) / 2; //父节点 = (子节点-1)/2
while (child > 0) //child从叶子结点开始,一直到根节点结束
{
if (arr[child] < arr[parent]) //如果孩子结点比父节点的值小,那么交换
{
Swap(&arr[child], &arr[parent]);//交换
child = parent;//child迭代
parent = (parent - 1) / 2;//parent迭代
}
else //如果孩子结点比父节点的值大,那么直接退出循环
{
break;
}
}
}
//向下调整(小堆)
void AdjustDown(ElemType* arr, int size, int parent) {
assert(arr);//断言,防止传入空指针
int child = parent * 2 + 1;//假设左孩子比右孩子的值小
while (child < size) //当child还没有到叶子结点,进入循环
{
if (child + 1 < size && arr[child + 1] < arr[child])//如果存在右孩子,并且右孩子比左孩子的值小
{
child = child + 1;//child为右孩子的位置
}
if ( arr[child] < arr[parent] ) {
Swap(&arr[parent], &arr[child]);//如果孩子结点比父节点小,交换位置
parent = child;//parent迭代
child = (parent * 2) + 1;//child迭代
}
else {
//如果父节点比子节点小,则退出循环
break;
}
}
}
//堆的插入
void HeapPush(Heap* hp, ElemType x) {
assert(hp);//断言,防止传入空指针
if (hp->capacity == hp->size) //如果容量为满或者容量为空,那么扩容
{
int newCapacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);//如果容量为空,那么就给4个空间; 否则将原来的容量扩大2倍
ElemType* temp = realloc(hp->arr, newCapacity * sizeof(ElemType));//用temp变量保存新申请的空间地址
if (temp == NULL)//如果空间开辟失败
{
perror("realloc fail!\n");
exit(1);
}
hp->arr = temp;//将temp赋值给arr
hp->capacity = newCapacity;//将新的容量newCapacity赋值给原来的容量
}
hp->arr[hp->size] = x;//将新元素插入
hp->size++;//数组的大小加1
AdjustUp(hp->arr, hp->size - 1);//新元素插入到叶子结点,采用向上调整算法
}
//堆的删除
void HeapPop(Heap* hp) {
assert(hp);//断言,防止传入空指针
assert(hp->size > 0);//断言,防止数组为空
Swap(&hp->arr[0], &hp->arr[hp->size - 1]);//将首尾元素交换
hp->size--;//删除最后一个元素(原来的堆顶元素)
AdjustDown(hp->arr, hp->size, 0);//将堆用向下调整算法重新排好
}
//取堆顶的数据
ElemType HeapTop(Heap* hp) {
assert(hp);//断言,防止传入空指针
assert(hp->size != 0);//断言,防止堆为空
return hp->arr[0];
}
//堆的有效数据个数
int HeapSize(Heap* hp) {
assert(hp);//断言,防止传入空指针
return hp->size;//返回堆的大小(有效数据的个数)
}
//堆的判空
int HeapEmpty(Heap* hp) {
assert(hp);//断言,防止传入空指针
return hp->size == 0;//判断堆是否为空
}
测试一下:
片尾
今天我们学习了堆这种数据结构,知道了什么是堆以及如何实现堆,希望能对友友们有所帮助 ! ! !
求点赞收藏加关注 ! ! !
谢谢大家 ! ! !