->有关堆的知识
(1)堆总是一个完全二叉树。
(2)堆分为大堆和小堆,大堆的意思是每个父亲结点都要比孩子结点更大或者等于,而小堆就是每个父亲结点都要比孩子结点更小或者等于,在大堆中最大的总是根位置的数,小堆同理类推。
(3)在插入一个数据后,堆的形式不变他依然还是一个符合原来性质的堆。
(4)堆是可以用来排序的,但是他并不是排序。
1.代码实现的目的
用代码完成堆的创立,删除,增加数据,删除数据,等常用功能。
2.代码的逐步实现(以大堆为例)
(1)堆的初始化和一些头文件
堆的形式与顺序表很相似,都是用数组来存储数据,但是一般也只有完全二叉树才使用数组来存储数据,这是因为普通的二叉树存入数组里面会导致空间利用不完全等问题。
用图的形式就是这样的一个样子:
由上图我们还可以得知,完全二叉树在数组中的存储是按层序存储的。
以下就是代码实现:
头文件等:
初始化代码:
(2)添加数据
堆在添加数据后依然还得是一个堆,我们的size是数组中数据的个数,添加数据我们可以把它添加到下标为size的位置上面去,但是这样我们会发现想要符合堆的性质,难免遇到交换位置的问题。
这个时候我们就需要用到一些关于树中的计算问题了:
在一棵二叉树(上图仅为一个普通的树)中,将A的下标为0,B的下标为1,然后按照这样的规律类推下去。在下标的运算中我们会发现,父亲结点的总是等于孩子下标-1之后在/2得到,而孩子结点则是父亲结点的下标乘以二再加1或二:
即:parent = ( child - 1 ) / 2; leftchild = parent * 2 + 1; rightchild = parent * 2 + 2;
便于交换位置我们可以单独写出一个swap函数:
然后函数的主题部分就如下实现:
下面解析一下这部分代码:
如果size == capacity,则说明数组的空间已经满了,则对它进行扩容的操作(我这里是扩大到原来的二倍)。然后在size下标位置添加新数,size的意义是反映数据个数,所以要++。最关建的是最后一个函数:
如果插入的数比它的父亲结点更大,那么必然是需要换位置的,这时候就是向上调整的过程。
这个函数需要传入新添加的数据的下标,然后利用parent = ( child - 1 ) / 2。
这里有一个注意的点就是循环的条件,这里建议用child>0来控制,有人会用parent来控制,实际上这是不正确的,这个代码的意思就是如果字节点比父亲节点大,那么交换位置,交换位置之后再重新新的一轮比较,若使用parent > 0
作为循环条件,当元素调整到根节点时,由于根节点的父节点索引是负数,在某些情况下可能会引发未定义行为,而且无法准确判断是否已经到达根节点。
(3)删除数据
试想删除数据我们需要删除最后的数据吗,那岂不是太简单了,直接size--就完事了,但是我们会发现这样没啥意义,所以我们删除堆顶的数据。然后让这个堆重新变成一个大堆。我们直接删除的话,会发现太难控制了,所以有一个删除的方法就是,把顶部数据和最后一个叶子数据交换位置,然后将交换后的数组再变成一个堆,最后再size--就可以了。
最核心的莫过于向下调整的函数了,这是大堆,交换之后要保持根部数据最大向下调整是必然的。由于前面的size减过一次,则传入的size就是剩余数据的个数,0则是根部数据的下标:
下面是代码实现:
向下调整的过程中需要比较父亲结点和孩子结点的大小,我们默认两个孩子中左边的孩子是较大的那一个,这样换上去之后他就是最大的了(因为原本就只有现在的根部数据不符合大堆的形式)。
这里的控制条件是child不能大于n也就是size要小于等于数组的最大下标。
(4)求堆顶数据
以下是代码实现:
(5)对这个堆判空
以下是代码实现:
(6)求有多少个数据
以下是代码实现:
(7)销毁堆
以下是代码实现:
3.结尾
由于后面几个函数的实现过于简单,便不再过多赘述。