某公司《技术笔试心得》中算法与数据结构部分,有这样一道题:
数据表中有1000000个元素,找出其中最大的10个元素,采用什么算法比较好?
堆排序?希尔排序?快速排序?直接选择排序?
答案是“堆排序”。
什么是堆排序?本文就是在学习堆排序中的思考总结。


1.什么是堆和堆排序?

经过查找资料,你应该至少了解:
堆是一种数据结构。堆中的数据按照数组的方式储存,逻辑结构却是按照完全二叉树。
每个根节点都大于或等于其左右孩子结点的值,称为大顶堆;反之称为小顶堆。

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,是一种不稳定排序。
堆排序的时间复杂度为o(nlogn),并不是最低的,但是堆集合了数组和二叉树的优势,既利用二叉树的逻辑减小了排序次数,又利用了数组的快速索引增加了数据访问速度,所以堆排序非常适合数据量很大的排序情况。

请问:
什么是完全二叉树?

2.堆排序步骤

很多人介绍的都比较好,我就不重复造轮子了:
图解堆排序
图解排序算法(三)之堆排序
堆排序

大致步骤可以理解为:
start
构建初始堆;
循环调整{
堆顶最大值和堆尾数据替换;
调整堆,使堆顶值最大;
}
end

堆排序相比选择排序的优势在于:
同样是寻找最大值,选择排序需要从头到尾找一遍o(n),而堆排序是在有序的大顶堆(或小顶堆)上寻找最大值,在交换最大值位置之后,只需要从堆顶的子节点中选择一个较大值即可找到整个堆的最大值。

当然,既然堆尾数据换到了堆顶,后续还要按着一条分支调整下去,保证堆的性质不被破坏。每次调整的时间复杂度为堆的深度o(logn):
n/2 * log2(n) + n/4 * (log2(n) -1) + … < nlog2(n);

3.构建堆详解

构建堆,就是把无序堆调整为有序的大小顶堆。

你看这样构造行不行(我原来就是这样想的):
从堆底进行比较,每x个数据选出x/2个最大值,直到堆顶即可构造出大顶堆。

但是,大顶堆要求上面的数据总比下面的大,按照上面的步骤,每次构造,在一个含有三个结点的最小二叉树中,只能送出一个最大值到上面竞选最大值。假如这三个结点都很大,就会出现“比较大的值”被雪藏,不能升高到很高的位置。这样会导致不符合大顶堆的性质,但是不会影响下一步堆顶最大值和堆尾交换这一步,影响的是下面的调整步骤!

假如是正常的大顶堆,我们只需要按照分支进行调整,调整的具体时间复杂度小于nlog2(n)。假如出现上面构建初始堆不充分的情况,我们寻找最大值就不能简单的从堆顶的子节点寻找,而是要像构建初始堆那样才能找到堆的最大值,非常麻烦。

那么应该怎么构建初始堆:
首先,从下往上,每个最小二叉树中的三个结点都要进行两次比较选出最大值放到根节点。假如当前根节点就是最大值,则不用交换,继续往上走;当走到堆的中间某个地方,不满足根节点大于子节点,就需要发生调整,根节点需要被子节点取代。根节点下来还不算完事,根节点比不过左子节点,也未必比得过左子节点的子节点,所以,还需要在下来的根节点为根节点的那个最小二叉树中的三个结点进行比较。
总之,每发生一次交换,就要继续往下走,以换下来的结点为根节点继续比较调整。
简单理解“堆排序”
图中,堆顶的2改为8……大致意思都懂哈

4.如何保证堆性质不被破坏?

堆顶和堆尾交换之后,堆的性质会被破坏,后续可以通过调整保证堆的性质不被破坏。

构建初始堆的过程中,从下往上比较,假如开始不需要调整根节点,就会一路往上走到某个中间节点。然后此时根节点小于子节点,发生了调整,然后一路往下进行递归调整。这是我们刚才说过的。那么,这个一路往下的调整,会保证堆的性质不被破坏吗?一路往下调整之后,再需要从下往上调整吗?
简单理解“堆排序”
以上图为例, a是被最大值赶下来的left,abc需要竞争最大值时a被c赶下来;然后ade重新竞争最大值,不幸的是a又被d赶了下来。我想问的是,竞争成功的d会对cbd这个最小二叉树有影响吗?这个问题可以转化为d会比c大吗?不会,c曾经压制de,之后c又通过abc竞争获得最大值,c理所当然是abcde中的最大值。一旦升上去,就不会再退下来。

所以,构建初始堆的过程中,可以分为两个进程,原来只有从下往上,一旦发生调整,多开启一个往下递归调整的线程,向上的线程继续。

5.代码示例

实现代码的过程中,我曾对数组从0开始还是从1开始犹豫,后来发现让数组从1开始(0位置空着)纯粹是增加工作量。

我想到了使用递归的方法,但是从哪个结点开始呢?最后我尝试从子节点,写了一个函数:给出子节点,计算该子节点的兄弟结点和根节点,然后进行比较。其中还有判断该子节点是左结点还是右节点。后来才发现,别人都知道从最后一个非叶节点开始比较,最后一个非叶节点的序号为n/2-1.假如根节点为x,子节点分别是2x+1,2x+2。比我上述用的方法更简洁、容易理解!

总之,假如一开始是自己实现的话,会遇到形形色色的问题,形成各种各样的脑回路,尝试各种方法。最后才发现,原来有这种好用的方法,大家都在用,自己怎么没有想到。这样,印象也会更加深刻一些!
代码:Martin的GitHub

参考/鸣谢:
常用排序算法总结(一)

相关文章: