挑战408——数据结构(18)——二叉堆的建立与排序算法分析

堆的建立与排序

堆的建立

我们在刚刚提到过一个问题:我们所有的操作都是建立在一个已经建好的堆中,但是现实中,我们怎么可能那么轻松就得到一个建好的堆呢,所以我们还是得自己在一堆毫无规律的数组中,自己建立一个二叉堆。
那么构建堆的最好方式是什么呢?(也就是我们怎么写一个**builtHeap()**的方法)
假设我们要把下面的一组数构建成二叉堆:
在这里插入图片描述
当然我们可以想到的第一个办法就是,利用我们刚刚讨论的enqueue(k)方法,将这些元素一个个的插入到空的堆中。那么这样的操作,插入一个元素的算法复杂度为O(log N),那么插入N个元素,算法复杂度为 O(N logN)
但,总会有更好的办法解决这个问题(暂时称为heapify())。具体做法如下:

  1. 按所给的数组元素,按原来的顺序编号(我们从1开始),然后按此顺序构建一个二叉树。
  2. 从树的最底层开始,寻找第一个带有孩子的节点(例如在 n/2处),对每个元素实行 down-heap操作。随后调整整个树。

步骤2转换成C++代码应该就是:

for (int i=heapSize/2;i>0;i--){
	downHeap(i);
}

下面图解整个过程:
首先,我们按顺序写出数组(下标从1开始),并将他们构建成一个二叉树,如图:
在这里插入图片描述
接着我们寻找我们的最底层的,含有孩子的节点(从数组上看,表现为位于最后的元素的父节点),也就是43,再看看我们for循环,此时,heapSize的值为9,int(9/2) = 4.如下图:
在这里插入图片描述
然后我们在for循环中执行down-heap操作(这里表现为将12摆放到正确的位置,下图的图一1,图二都需要交换,图三不需要)!
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
注意,当i 等于1的时候,此时就到了小根堆的堆顶,此时再执行down-heap操作,就是删除栈顶的操作!
在这里插入图片描述
在这里插入图片描述
最终将整个二叉堆重构。
在这里插入图片描述

堆排序

堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。我们从刚刚的builtHeap()中看到,虽然我们完成了堆的构建,但是从存储的数组中可以看出,这是一串无序的数字,我们希望这串数字能保持堆结构的同时,还应该是有序的,这样方便我们从中取出相应的元素。这个过程就是堆排序的过程
我们可以通过以下的步骤来实现一个堆的排序。:

  • 首先,heapify一个数组(即在未排序的数组上调用build-heap,建立一个堆)
  • 其次,遍历整个数组并执行dequeue()操作,但注意此时不是返回最小元素,而是与最后一个元素进行交换
  • 循环完成后,数组将从低到高排序。
  1. 假定给出一个无序的数组:
    在这里插入图片描述
  2. 调用bulidheap()方式来创建新的堆结构。
    在这里插入图片描述
    在这里插入图片描述
  3. 循环遍历堆,并调用dequeue操作(此时堆根为2,将2移出),将根元素(2)与堆末尾的元素交换(43),heapSize()减一:
    在这里插入图片描述
  4. 此时按照dequeue()原理,根应该变为了5,heapSize由于减一,相应的堆末尾变成了22,如下图:
    在这里插入图片描述
  5. 继续执行3的操作,变成:
    在这里插入图片描述
  6. 重复3-5,直到heapSzie为0的时候,堆排序完成。(绿色部分代表已经完成的排序)
    在这里插入图片描述

这里我们注意,堆排序最终得出的结果也一定是一个堆,因为每交换一次数据,就会调用一次dequeue()方法,而这个方法需要将堆的结构重构,因此最终的出的数组序列也一定是个堆结构,且同一个父节点的左节点的值一定大于右节点的值

算法复杂度分析

前方高能!!! 一大批数学公式即将到来

BuildHeap的算法复杂度分析

现在让我们仔细地分析BuildHeap的运行时间。 像往常一样,通过对n进行一些假设,可以使我们的分析变得简单。 在这种情况下,最方便的假设是n的形式为 n = 2 h + 1 − 1 n = 2^{h+1} -1 n=2h+11,其中h是树的高度。 原因是具有此数量节点的左子树整树是完整树,即其最底层已满。 这种假设将使我们不必担心树的底部和树底的节点。

基于这个假设,这棵树的第0层含有1个节点,第一层有2个节点,…第h层有 2 h 2^{h} 2h个节点。回想一下,在调用Heapify时,运行时间取决于元素在流程终止之前从堆顶向下移动的距离。 在最坏的情况下,元素可能会一直向下移动到树叶。 让我们逐级计算完成的工作。

在最底层有 2 h 2^{h} 2h个节点,但是我们都不在其中任何一个上调用Heapify(因为已经是最底层了,无法再往下移动),因此工作量为0。在最底层的下一个有 2 h − 1 2^{h-1} 2h1个节点,因此最坏的情况下该层的节点都可能会向下移动1个层。 在从底部开始的第3层, 有 2 h − 2 有2^{h-2} 2h2个节点,每个节点可能会向下移动2个层。如下图所示:
在这里插入图片描述
通常,从底部往前数,第j层(从0开始计)的节点有 2 h − j 2^{h-j} 2hj个节点,并且这些节点都可能要往下移动j层,因此我们从下往上一层一层的数,总次数为:
在这里插入图片描述
将公因式 2 h 2^h 2h提取出来,于是得到:在这里插入图片描述
这是我们从未见过的总和。 我们可以尝试通过积分来近似它,这涉及到分部积分的知识。但是事实证明,对于这个特定的总和有一个特别的解决方案。 首先,写下任意常数x <1的无穷大几何序列(高数的无穷级数学过):
在这里插入图片描述
两边同时求导,之后再乘上X有:
在这里插入图片描述
这里令x = 1/2,那么就有:
在这里插入图片描述
因此,我们知道上述的无穷级数是有界的,因此我们得出以下结论:
在这里插入图片描述
此时我们再将 n = 2 h + 1 − 1 n = 2^{h+1} -1 n=2h+11,代入上式,则有:
在这里插入图片描述
因此BuildHeap的算法复杂度为O(N)
暂时先停留片刻,观察我们的二叉树,我们可以很明显的看到,绝大多数节点位于树的最低层。例如,在高度为h的完整二叉树中,总共有 n = 2 h + 1 n=2^{h+1} n=2h+1 个节点,但是仅底部3个层中的节点数就是:
在这里插入图片描述
也就是说,完整二叉树的几乎90%的节点位于3个最低级别。因此,要吸取的教训是,当设计在树上运行的算法时,在树的最底层(如BuildHeap一样)最有效是很重要的,因为这是树的大部分权重所在。

HeapSort的算法复杂度分析

回想一下**,HeapSort算法的操作方式是首先以自底向上的方式构建一个堆,然后反复从堆中提取最大元素并将其移到数组的末尾。该算法的特别之处在于,在保证堆结构的前提下,所有的元素都是按序排列的**。
我们知道,Heapify的基本堆操作运行时间为O(log n),因为堆具有O(log n)级别,并且经过恒定工作量后,被筛选的元素向下移动到树的一级。基于此,我们可以看到堆排序中,我们需要从堆中提取N个元素,并且每次提取都需要一定数量的工作,并且,提取完毕之后我们需要对这个元素进行Heapify操作,使之重构。因此HeapSort的总运行时间为O(n log n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值