⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:数据结构初阶
⭐代码仓库:Data Structure
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
前言
我们知道,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费(这中间有很多空的空间)。而完全二叉树更适合使用顺序结构存储。在现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。我们看下图完全二叉树和普通的二叉树导致的空间的浪费。
一、三板斧
老规矩,还是三板斧,创建两个.c文件和一个.h文件,养成封装文件的习惯:
二、堆的结构
这里我们使用堆的结构为数组的结构,也就是数组存储堆上的数据,堆上数据满了就扩容,那我们就需要一个size记录数组的长度,capacity记录数组的容量,*a为数组,如下图:
三、初始化堆
初始化堆即将堆中的结构都初始化实现一下即可,我们先给数组4个整型空间,容量此时为4,size为0:
四、堆的销毁
有创建堆那必须要有销毁堆的函数,因为这样才能不消耗计算机内存的空间,尽早的释放好处多多。
五、堆的插入与向上调整算法
这里我们进行书写堆的插入与向上调整算法。那我们先写一下堆的插入,我们想一想之前写的顺序表和链表,其插入有尾插、制定插入和头插,而我们这边堆的插入的话是尾插,也就是在数组的后面进行插入,至于为什么,这是因为堆的算法是这么规定的,而我们想要形成堆的话其实很简单了,因为尾插,所以就需要写一个向上调整算法,将数据往上调整,形成一个大根堆或者小根堆即可,因为插入的数据不一定是一个堆的数据,有可能是一个最大的数据,也有可能是中间数据,那就需要我们进行向上调整将数据放在应该在的地方,形成一个堆,我们实现实现一下堆的插入,再实现一下向上调整算法:
堆的插入其实很简单,我们需要先判断堆是否已经满了,也就是size和capacity是否相等即可,满了就扩容,没满那我们就开始给数组进行尾插即可,只需要在尾部进行插入。
我们现在插入这几个数据:
那这个数据插入以后,不确定这个值的大小是否是堆上的数据,那我们就需要写一个向上调整的函数,也就是要把这个数据放到上面去,我们看图看一下规律吧!
所以我们记住了一个公式:
parent = (child - 1)/2
我们直接上手写一下我们的向上调整函数吧:
六、堆的删除与向下调整算法
我们进入到堆的删除的时候,其实发现很难受的一点是,我们删除末尾元素有用吗?我们把下图中的1删除有用吗?似乎好像没什么用啊因为它还是一个堆,就好在一个山头上,有老虎头头,老虎二当家……我们此时想把头头干掉,扶植二当家当头,让二当家去引领整个老虎群,这样才有意义,因为这样我们选出的第二大的数据,为什么我们非要选出第二大的数据??不简单删最末尾的数据呢?原因是在于我们后续会用到堆排序,需要进行排序,找二当家的位置,所以我们需要删除老大,让老二去当头。
既然我们有了思路了,那我们怎么去实现呢?其实我们思考一下,如果单纯地将76一删,也就是头删的话,那会导致一个很明显的问题,关系网乱了,我们看38和37这两个数据,明显38大,那就直接将38扶植成为头头,那38上去了,31也跟着往上走了,那关系全乱套了,38变成37的父亲了,这俩以前还不是兄弟吗?这怎么变成父子关系了?乱套了,所以我们不能直接头删。那此时就有一种非常好用的算法了,先将第一个元素的值和最末尾元素的值进行交换,然后直接删除最末尾的值,再将顶上的值进行向下调整即可,但这个算法的局限性在于必须是两个子树都为堆才行,也就是顶头祖先的两个分支的这两个子树必须是堆才行,那我们进行画图解释一下:
记住公式:
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
前面已经写过Swap函数了,我们这里就直接封装函数即可。其次,这里注意需要判断此时的堆是否为空,我们函数放在下面讲解。
七、取堆顶的数据&堆的数据个数&堆的判空
三个放在一起即可,因为这三个很简单:
八、原码
Heap.h:
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
#include<string.h>
// 根部下标从0开始的完全二叉树
// parent = (child - 1)/2
// leftchild = parent * 2 + 1
// rightchild = parent * 2 + 2
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* hp);
//堆的销毁
void HeapDestory(Heap* hp);
//堆的插入
void HeapPush(Heap* hp, HPDataType x);
//堆的删除
void HeapPop(Heap* hp);
//取堆顶的数据
HPDataType HeapTop(Heap* hp);
//堆的数据个数
int HeapSize(Heap* hp);
//堆的判空
bool HeapEmpty(Heap* hp);
//向上调整
void AdjustUp(HPDataType* a, HPDataType child);
//向下调整
void AdjustDown(HPDataType* a, HPDataType n, HPDataType parent);
//交换数据
void Swap(HPDataType* p1, HPDataType* p2);
Heap.c:
#include"Heap.h"
//堆的初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (hp->a == NULL)
{
perror("malloc fail");
return;
}
hp->size = 0;
hp->capacity = 4;
}
//堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
while (!HeapEmpty(hp))
{
hp->size--;
}
free(hp->a);
hp->a = NULL;
hp->capacity = 0;
}
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//向上调整(除了child,其余全是堆)
void AdjustUp(HPDataType* a, HPDataType child)
{
//判断孩子和父母的关系
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//迭代
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
//判断是否满
if (hp->capacity == hp->size)
{
HPDataType* temp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity * 2);
if (temp == NULL)
{
perror("realloc fail");
return;
}
hp->a = temp;
hp->capacity *= 2;
}
//插入
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
//向下调整
void AdjustDown(HPDataType* a, HPDataType n, HPDataType parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的删除(前提是左右子树都是堆)
void HeapPop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
//删除根部
//先将根部数据与最末尾元素调换一下,再size--
//最后向下判断
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
//取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->a[0];
}
//堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
//堆的判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
Test.c:
#include"Heap.h"
int main()
{
Heap hp;
HeapInit(&hp);
HeapPush(&hp, 1);
HeapPush(&hp, 31);
HeapPush(&hp, 21);
HeapPush(&hp, 20);
HeapPush(&hp, 4);
HeapPush(&hp, 76);
HeapPush(&hp, 38);
HeapPush(&hp, 37);
HeapPush(&hp, 30);
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
printf("\n");
HeapDestory(&hp);
return 0;
}
总结
堆就是二叉树,我们进行建堆的时候就是相当于建了一个二叉树,利用二叉树的parent和child的下标的关系即可实现我们之前没有接触过的向下和向上调整的算法,这样子为后面进行堆排序的理解有很好的基础,理解起来更加方便。
家人们不要忘记点赞收藏+关注哦!!!