一:堆的概念
-
完全二叉树:堆总是一个完全二叉树
-
堆属性:在堆中,每个节点的值都必须大于或等于其子节点的值(这叫大堆),或者小于或等于其子节点的值(这叫小堆)。也就是说,大堆的根节点是整个堆中最大的元素,而小堆的根节点是整个堆中最小的元素。
-
本文博主实现小堆,以小堆为例来讲解
二:堆的实现的原理
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:向下调整中