堆的实现(看这一篇就够了)

一:堆的概念

  1. 完全二叉树:堆总是一个完全二叉树

  2. 堆属性:在堆中,每个节点的值都必须大于或等于其子节点的值(这叫大堆),或者小于或等于其子节点的值(这叫小堆)。也就是说,大堆的根节点是整个堆中最大的元素,而小堆的根节点是整个堆中最小的元素。

  3. 本文博主实现小堆,以小堆为例来讲解

二:堆的实现的原理

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆 (完全 二叉树 ) 使用顺序结构的数组来存储

1:用数组来存储堆,可以得到以下的三个公式

leftchild  = parent *2 +1(左孩子的下标等于父亲的下标*2+1)

例:D的下标 = B的下标1 去*2 + 1 = 3
rightchild = parent *2 +2(右孩子的下标等于父亲的下标*2+2)

例:E的下标 = B的下标1 去*2 + 2 = 4

parent = (child-1)/2*(父亲的下标等于孩子的下标-1再/2)

例:B的下标 = D的下标-1再/2 = 1 或 E的下标-1再/2 =1(int的运算法则

2:堆的插入

实现堆包含插入函数,因为是堆的实现,所以我们要确保每次插入后,他都还是一个堆,所以我们需要用到一个向上调整函数(AdjustUp)

插入的几种情况:

第一种:插入的孩子节点直接大于父亲(90>56)

第二种:插入的孩子节点小于父亲,但未小于根节点(50<56 但 50>10)

第三种:插入的孩子节点直接小于了根节点(5<10)

所以 向上调整函数(AdjustUp)的本质:

当孩子小于父亲,即交换,以此类推.......

3:堆的删除(删除根节点)

实现堆包含删除函数,因为是堆的实现,所以我们要确保每次删除后,他都还是一个堆,所以我们需要用到一个向下调整函数(AdjustDown)

删除的演示:

所以 向下调整函数(AdjustDown)的本质:

1:先交换根节点和尾节点,然后删除尾节点(即起到删除根节点的作用)

2:然后新的根节点作为父亲(90作为父亲),与左右孩子中的最小一个比较(90与15比较),如果大于最小的孩子(90>15),即交换,以此类推.......

3:最后新的堆的元素个数会比删除前少一个,因为最后一个空间被抹去 

注意:向上/向下调整得要求左右子树都是大堆或者都是小堆

三:堆的实现

第一点:
本文采用三个文件进行实现
1:Heap.h(堆有关哈函数的声明)
2:Heap.c(堆有关哈函数的实现)
3:Text.c(对函数的检测)

第二点:

所有的函数都要对接受到的结构体指针的地址(本文统一php)进行assert,确保地址不为空。(因为就算结构体为空,其地址也不会为空)而且部分还会会assert堆中是否存在元素。

四:函数的实现讲解

前提:堆的结构体的声明

解释:

1:我们用数组来实现堆,所以要有一个 数组a

2:size代表堆中元素的个数,capacity代表堆的容量

3:以下的函数需要改变堆这个结构体中的时候,都需要向函数传结构体指针(php),才能真正的修改

第一个函数:初始化堆

解释:

1:断言php

2:初始化结构体中的各项

第二个函数: 交换函数

解释:

1:在向上调整和向下调整中都会用到次函数,即交换父亲和孩子的值,所以需要用到指针

第三个函数:插入函数

解释:

1:插入先判断是否装满

2:

 不管是哪种情况,都会去开辟空间给a,所以我们采用realloc。此处两个重点:

Q1:为什么第一次开辟不用malloc?

A1:我们后面会在a的空间基础上进行再次开辟,所以realloc才对,其次我们初始化的时候a = NULL了,在realloc函数中,接收的指针为NULL,realloc就等同于malloc了,也就相当于第一次开辟用的是malloc。

Q2:为什么不直接用a来接收开辟的空间,而是用了一个tmp中间变量,最后在赋值给a;为什么不用capacity直接接收三目操作符的返回值,而是中间变量newcaoacity?

A2:如果用a接收,如果realloc开辟失败,但是a如果之前不是空栈,那之前的栈元素也会丢失,而用一个tmp去接受,再去判断是否开辟成功会更好保护a之前的数据。capacity同理。

3:

经过1,2步后,确保有容量了,再进行赋值和size的++,最后进行向上调整,才能成为一个堆。

第四个函数:向上调整函数(AdjustUp)

解释:

1:由上文我们对向上调整函数的分析可知,:当孩子小于父亲,即交换,以此类推.......

Q1:为什么while条件不是parent>=0

A1:

在第三种情况中:

当5位于根节点的时候,此时根据我们的代码,parent的下标会更新为 (0-1)/2,还是0,所以parent的下标即使到根节点,再根据公式往下走,也不会<0 ,无法终止循环。

所以用child来判断(child>0),当child的下标为0的时候,代表parent下标为0的这一个已经比过了,并且二者交换了,即终止循环。

2:

当孩子小于父亲的时候,根据小堆的特性,就不用继续比对了,肯定比上面的祖先都要小。

第五个函数:删除函数(删除根节点)

解释:

1:断言size是因为删除的前提是确保有元素

2: 根据前文的向下调整的思路,先交换根节点和尾节点,在抹去尾节点,然后向下调整

第六个函数:向下调整函数

解释:

1:向下调整的极限是child位于最后一个元素,即下标达到最大值n-1,(n就是size)(比如6个元素,下标最大只能取到5),所以child只能小于n,不可能大于或等于n

2:但是在确定孩子中较小值的时候,会用到a[child+1,]来判断,所以为了避免超出范围,确定范围的这个if应该是&&,左面确保child+1<n,左面成立,才会执行&&右面的比较,否则会越界访问

3:不能为了将就第一个if,而在while的条件直接 child<n-1,因为如果child的下标会更改为最后一个元素,此时child 的值为n-1,此时会因为错误的判断,而不进入循环判断,应该是先进入循环,不进去刚才的if,最后再让那一个孩子和他比较。

第七个函数:取堆顶数据

解释:

1:第二个断言确保有元素,然后直接返回数组的第一个元素

第八个函数:判空

解释:

size为0,即为空。

第九个函数:堆的数据个数

解释:

size即代表堆的数据个数

第十个函数:堆的打印

解释:

循环打印每一个元素即可,%d后跟一个空格更容易观察。

第十一个函数:销毁堆

解释:

将结构体中的一切恢复初始即可。 

最后是text.c对函数进行一系列测试的运行结果:

由代码可知:一组随机的数组,经过我们的函数,变成了一个小堆,如下图: 

不断地取堆顶数据,然后删除堆顶,再取堆顶,可以依次取到最小的k个值 。

如果要实现大堆,只需要改变几个符号即可

1:向上调整中

2:向下调整中

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值