堆是利用 完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在O(1)~O(logn)之间。我们在了解学习堆这个数据结构之前,必须先学习一些二叉树的概念和性质。
二叉树
二叉树的概念
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
简单来说,二叉树就是每个节点的度(树的度)小于等于2的数
满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
简单来说,满二叉树就是每个结点的度都为2,每个结点都有两个孩子结点
完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
堆
堆的分类
堆分为:
1.小根堆
2.大根堆
(1)小根堆:小根堆的任一非终端节点的数据值均不大于其左子节点和右子节点的值。
每个结点的数值一定小于它的子节点的数值
(2)大根堆:与小根堆相反,根节点的数值不小于其左右子节点的数值
如何建堆(将数据变成堆的存储方式)
我们以小根堆为例(大根堆的话,就是将对应位置的 < 变为 > )
情况一:根的左子树、右子树都恰好是小根堆
采用“向下调整法”
步骤一:首先对于要操作的堆的根节点,找出根节点的孩子节点中数值较小的孩子节点
步骤二:将较小的孩子节点的数值与根节点的数值进行比较
a.若根节点的数值大于较小的子节点的数值,那么将这两个结点的数值进行交换,使小的数交换到根节点去(就是为了使根节点的数值最小)然后进行步骤三
b.若较小的孩子结点的数值大于根节点,则直接进行步骤三
步骤三:进行迭代,将步骤一中求出的较小的孩子结点作为下一次迭代中的根节点继续重复以上步骤,直到走到最后的末端节点(叶子节点)。
//代码实现
void AdjustDown(int* a, int sz, int parent)
{
int child = 2 * parent + 1;
while (child<sz)
{
if (child<sz - 1 && a[child]>a[child + 1])
child++;
if (a[child] < a[parent]) //如果双亲大于孩子,就不对,就要交换
{
Swap(&a[child], &a[parent]); //将两值交换
parent = child;
child = 2 * parent + 1;
}
else //存在一次孩子都小于双亲,(由于根节点的两个子树都是小根堆)后面肯定符合小根堆,所以就不用循环了
break;
}
}
hehe
情况二:一般情况,左右子树不是小根堆
分析:我们可以将左右子树先建成小根堆,左右子树建成小根堆又分为相同的情况一和情况二。如果还是情况二,就将它的左右子树建成小根堆。如下重复
所以,总结起来,就是将整个二叉树的从最后一个非叶子结点开始,从后往前,按编号,依次将每个结点都作为一个根来建堆,直到整个二叉树的根节点结束。
//代码实现
void HeapBuild(int* a, int sz)
{
int child = sz - 1; //找到最后一个叶子节点
for (int i = (child-1) / 2; i >= 0; i--) //child为最后位置的叶子节点,根据公式:左孩子 = 双亲*2+1 右孩子 = 双亲*2+2
//(child-1)/2为其双亲结点
{
AdjustDown2(a, sz, i); //由于将一个根节点的两个子树都建好堆,所以问题退化成情况一
} //对于叶子结点的双亲结点作为“根”的时候,也相当于它的左右叶子结点为堆
}
建堆的复杂度分析
![](https://img-blog.csdnimg.cn/20210528203306486.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nkemdfenpr,size_16,color_FFFFFF,t_70hehe由于堆是完全二叉树,而满二叉树是完全二叉树中结点数最多,相当于最复杂的情况,所以我们以满二叉树为例子,分析建堆的复杂度。
hehe建堆需要我们从最后一个分支点(即最后一个非叶子节点)开始向下调整算法(详见上述内容),直到调整到根结束。
hehe
堆排序算法
**
分析为啥可以用堆来排序捏?
分析为啥可以用堆来排序捏?
堆中的第一个元素一定是所有数据中最大/最小的数据。所以我们可以通过一次一次排序将最大/最小的数据得到,重复循环从而得到一个有序数组。
我们以将数据排序成升序来举例
####灵魂问题####我们将数据排成升序,应该将堆建成大堆还是小堆捏?
有的同学说:肯定是小堆啊:只有小堆才能获得最小的数据放在最前面,再将剩下的数据重复建小堆,就可以了啊
有的同学说:这么弱智的问题都问出来了,肯定不简单,所以我认为要建大堆!!
于是我们就要恭喜投机取巧,揣测出题人意图的同学们回答正确了!!
下面就要着重解释一下为啥要建大堆了
hehe
上图是已经建好的小堆,我们获取根节点的元素作为最小的数值,剩下的结点如下图
我们可以发现,这完全是一个乱序的二叉树,属于情况二,我们需要完全重新建堆。时间复杂度飙升至O(N*N),所以说,效率是极低的。那如果采用大堆排序呢?
建完大堆后,我们需要将第一元素和最后一个元素互换位置
注:1.目的是将第一个元素(最大元素)移动到最后位置。 2.最后元素不一定就是最小的元素
此时前n-1个元素是符合情况一的,所以再建堆的时候只需要进行一次向下调成操作,复杂度就变为O(N*logN)
//代码实现
void HeapSort(int* a, int sz)
{
HeapBuild(a, sz); //先建大堆
int end = sz - 1; //始终指向剩余元素的最后位置,既方便交换,
//又可以监视排序是否完成
while (end>0)
{
Swap(&a[0], &a[end]);
AdjustDown2(a, end, 0); //建大堆的向下调整方法
end--;
}
}