简介
关于堆排序的文章,可以说网上一搜就有一大堆。有的时候自己都在想有没有写这个的必要。仔细看看网上的一些文章,很多不外乎一上来就直接堆代码,让人看的云里雾里。有的则是讲的比较笼统,让人很难懂。于是就想根据自己学习思考的经历,尽量用一种容易理解的方式整理出来。也当是一种学习总结吧。
关于堆
一般我们看到堆这个词,总会想到那些分配对象存储等复杂的数据结构。在堆排序里,很直白的来说,堆就是一个简单的数组。只是我们用一种完全二叉树的角度来看它。以最大堆为例,比如说我们有一棵如下的二叉树:
从上图中,我们发现,如果我们从根结点开始按照从左到右一层一层的编号的话,对这些元素的访问就构成了一个序列。比如上图中的序列按照编号顺序如下:16, 14, 10, 8, 7, 9, 3, 2, 4, 1
如果我们将这种从二叉树的结点关系转换成对应的数组形式的话,则对应的数组如下图:
从二叉树的每个结点的编码到它的左右字结点的关系,我们发现一个有意思的地方:
左子结点的编号=父结点编号 * 2
右子结点的编号=父结点编号 * 2 + 1
按照数组标的编号,有类似的对应关系:
左子结点的数组索引号= 父结点索引号 * 2
右子结点的数组索引号=父结点索引号 * 2 + 1
这样,我们通过一定的运算对应关系将二叉树关系的元素存储到一个数组中。针对以上的父子结点关系,他们对应的求法可以用一下几个方法实现:left(), right()。在实现的时候考虑到我们的数组下标是从0开始的,对应的关系修改为:
left(n) = n * 2 + 1 right(n) = n * 2 + 2
对应的代码实现如下:
left:
public static int left(int i) { return i * 2 + 1; }
right:
public static int right(int i) { return i * 2 + 2; }
调整堆
ok,前面我们已经理解了堆和对应的数组之间的关系了。再来考虑另外一个问题。我们假定是要建立一个最大堆。那么对于这么一个最大堆来说,它有一个重要的特性就是处于父结点的值必须比它的子结点要大。假设某一棵树上面的父结点不满足这个要求,我们就必须进行调整。笼统的说,调整就是将这个不符合条件的结点和子结点进行比较,通过交换将最大的结点作为父结点。具体的流程见下图:
在上图中,我们发现值为4的结点不符合要求。那么就需要进行交换调整。接着就需要在它的两个子结点中选择最大的那个,然后交换位置。它的子结点中最大的是14.交换之后的结果如下图:
经过交换之后,我们发现原来元素所在的位置确实符合要求了。可是4交换到新的结点之后又不符合最大堆的条件了。没办法,还需要继续选择最大子结点进行交换。这么一交换之后的结果如下:
经过这么两轮交换,我们终于可以保证以i = 2这个结点为根的树最终达到了一个符合最大堆的状态。总结前面这么一个交换调整的过程,主要如下:
1. 比较当前结点和它的子结点,如果当前结点小于它的任何一个子结点,则和最大的那个子结点交换。否则,当前过程结束。
2. 在交换到新位置的结点重复步骤1,直到叶结点。
对上面的过程进行细化之后编码,我们可以得到两个版本的方法:
递归版本:
public static void maxHeapify(int[] a, int i) { int l = left(i); int r = right(i); int largest = i; if(l < a.length && a[l] > a[i]) largest = l; if(r < a.length && a[r] > a[largest]) largest = r; if(i != largest) { swap(a, i, largest); maxHeapify(a, largest); } }
非递归版本:
public static void maxHeapify(int[] a, int i) { int l = left(i); int r = right(i); int largest = i; while(true) { if(l < a.length && a[l] > a[i]) largest = l; if(r < a.length && a[r] > a[largest]) largest = r; if(i != largest) swap(a, i, largest); else break; i = largest; l = left(largest); r = right(largest); } }
以上两个版本的实现主要有几个要点要注意:1. 每次求一个结点的子结点的时候要检查是否越界。 2. 每次通过将当前结点和子结点的比较来选取最大值,如果最大值就是当前结点,则程序返回。 3. 里面的swap方法就是交换两个索引位置元素的位置,因为比较简单就在此省略了。
建最大堆
我们注意到,前面虽然有一个堆调整的过程,但是这个过程主要针对的是一个树中的一个结点。如果树中间有多个结点不符合最大堆的条件,我们光调整某一个结点是没有用的。那么,就需要一个办法来将整棵树调整成符合条件的最大堆。
一个最简单的办法就是从最低层的结点开始起调整。很明显,如果我们从a[a.length -1]这样的结点来调整的话,有相当一部分结点是没必要的。因为这些结点很显然是叶结点,也就是说他们根本就没有子结点,连找子结点和去比较的必要都没有了。所以,我们可以从最后面往前到过来去找那些有子结点的结点,然后从这些结点开始一个个的进行堆调整。那么,我们该从哪个结点开始起进行调整呢?另外,我们可能还有这么一个疑问,为什么我们要从后面往前去调整?从前面往后调整不行吗?别急,让我们一个个的来分析。
首先第一个问题,从哪个结点开始进行调整。我们来看这棵二叉树,很显然,它最后的一个元素也肯定就是最终的一个叶结点。那么取它的父结点应该就是有子结点的最大号的元素了。那么从它开始就是最合适的。取它的父结点可以通过一个简单的i / 2来得到,i为当前结点的下标。
然后我们再来看第二个问题,为什么要从后往前而不是从前往后。这个相对也比较好理解。我们从下面的层开始调整,保证当上面的父结点来调整的时候,下面的子树已经满足最大堆的条件了。这样出现不符合条件的父结点只需要用前面的maxheapify过程就可以。而从前面往后调整呢,我们看下面的一个示例:
如果我们从根结点开始,根结点元素4比它的两个子结点都大,不需要调整。而再往后面的时候它的子结点1调整之后被换成16.这样就出现了它的子结点比它还要大的情况,因此从前往后这么调整的过程不行。
经过前面的讨论,构建最大堆的过程就相当的简单了:
public static void buildMaxHeap(int[] a) { for(int i = a.length / 2; i >= 0; i--) maxHeapify(a, i); }
总的来说,建立最大堆的过程无非就是要建立一个符合如下条件的二叉树:它所有的结点值都比它的子结点要大。
拼在一起
好了,有了前面扯的基础,再来看怎么排序就很好理解了。既然我通过建了一个最大堆,能够保证最大的元素就是根结点,那么,我们如果要从小到大排序的话,最大的元素就只要取根结点就可以了。如果我们把根结点拿走了,放到结果集的最末一个元素,接着就应该找第二大的元素。因为要保证这棵树本身是近似完全二叉树的性质,我们不能把中间的结点直接挪到根结点来比较。但是前面的maxHeapify过程提醒我们,如果我们从集合的最低一层叶结点来取,然后放到根结点进行调整的话,肯定也是可以得到剩下元素里面的最大结点的。就这样,我们可以得到这么一个过程:
1. 取最大堆的根结点元素。
2. 取集合最末尾的元素,放到根结点,调用maxHeapify进行调整。重复步骤1.
在具体实现的时候我们可以发现,每次都要取集合中后面的元素,我们原来得到的最大结点正好可以放到集合的末尾,正好达到最大的元素放到最后的效果。
实现堆排序的临门一脚如下:
public static void heapSort(int[] a) { if(a == null || a.length <= 1) return; buildMaxHeap(a); int length = a.length; for(int i = a.length - 1; i > 0; i--) { swap(a, i, 0); length--; maxHeapify(a, 0, length); } }
仔细看前面的代码,大家可能会发现一个细小的改变。就是maxHeapify方法多了个参数。这是因为考虑到实际情况下,如果每次我们把找到的当前集合最大元素放到后面了,那么这些元素就相当于从前面的集合中排除出来,后面进行堆调整的时候就不需要再考虑。所以用一个length的长度来限制调整的范围,以免伤及无辜:)。具体的实现可以看后面附件里的详细实现代码。
总结
堆排序粗看起来有好多个步骤,又是要建堆又是要调整的显得很复杂。其实它的过程概括起来无非就是这么两个步骤,一个就是建一个堆,然后就是每次取走根结点的元素用后面的来补,补上后进行调整,然后再重复前面的步骤。
相关推荐
总结,`arm_max_f32`和`arm_sort_f32`是针对32位浮点数数组的实用函数,分别用于查找最大值和进行排序。通过理解这两个函数的参数和工作原理,开发者可以在MSP432平台上有效地处理和分析浮点数数据。
总结来说,"sort.cpp"是一个有价值的教育资源,它将理论知识与实践结合,帮助编程爱好者和初学者更好地理解和掌握排序算法。通过阅读、运行和修改这个程序,不仅可以提升编程技能,还能深化对算法原理的理解,对于...
数据结构课程设计排序实验报告 本报告的主要内容是设计一个数据结构的排序实验报告,...本报告的主要贡献是设计和实现了多种排序算法,并且对它们进行了比较和分析,为读者提供了一个 Sorting algorithms 的实践指南。
4. 堆排序(Heap Sort) 堆排序是一种高效的排序算法,通过将数组转换成堆,然后不断地将堆的根元素与最后一个元素交换,直到整个数组有序。其时间复杂度为 O(n log n)。 5. 归并排序(Merge Sort) 归并排序是一...
总结来说,"sort算法比较(实验4).rar_C 时间比较"是一个关于使用C语言实现和比较不同排序算法的实践项目,主要目的是通过实际运行和时间测量来评估各种排序算法的效率。实验结果对于理解和优化算法性能,尤其是在...
排序算法是用来组织和优化数据结构的关键工具,它可以帮助我们快速查找、分析和处理大量数据。本篇将深入探讨Java中实现的几种主要排序算法,并提供相关的范文、模板和素材。 1. 冒泡排序(Bubble Sort) 冒泡排序...
本资源"排序算法总结"提供了对10种基本排序算法的详细讲解和性能分析,这对于学习者来说是一份宝贵的资料。以下是对这些排序算法的详细介绍: 1. 冒泡排序(Bubble Sort):冒泡排序是一种简单的交换排序,通过重复...
堆排序(Heap Sort)基于完全二叉树的特性,构建最大或最小堆,然后将堆顶元素与末尾元素交换,调整堆,重复此过程,时间复杂度为O(n log n)。计数排序(Counting Sort)、桶排序(Bucket Sort)和基数排序(Radix ...
6. 堆排序(Heap Sort) 堆排序利用了完全二叉树的特性构建一个大顶堆或小顶堆,然后将堆顶元素与末尾元素交换,再调整堆,重复此过程。堆排序在最坏、最好和平均情况下的时间复杂度均为O(n log n),但它是原地排序...
这些排序算法广泛应用于各种场景,如数据库、数据分析和算法竞赛。 1. 冒泡排序(Bubble Sort) 冒泡排序是最简单的排序算法之一,它通过不断交换相邻的逆序对来逐步将大元素“冒泡”到数组的一端。时间复杂度为O(n...
总结来说,堆排序是一种高效的排序算法,它的核心在于构建和调整堆的过程。通过C Profiler的使用,我们可以深入理解算法的执行效率,并针对瓶颈进行优化。同时,HTML标签的应用则有助于以更友好的方式展示分析结果。
堆排序(Heap Sort) 堆排序利用了二叉堆数据结构,将待排序序列构造成一个大顶堆或小顶堆,然后交换堆顶元素与末尾元素,再调整堆。时间复杂度为O(n log n),空间复杂度为O(1),但排序过程中的数据交换较为频繁。 ...
【互联网面试常见算法总结】 在IT行业的面试中,算法题是评估候选人技术能力的重要环节,...在面试准备时,不仅要理解算法的逻辑,还要熟悉其时间复杂度和空间复杂度分析,以及如何根据具体问题选择合适的排序算法。
这份压缩包文档可能提供了每种算法的实现代码、示例和性能分析,对于初学者和进阶者都是宝贵的参考资料。不过,由于未提供实际文档内容,以上内容只是基于标题和标签的推测。如需深入学习,建议解压文件后详细阅读。
在计算机科学领域,排序算法是数据结构与算法中不可或缺的一部分,它主要负责将一组无序的数据按照特定的顺序进行排列。...理解这些排序算法,不仅有助于编程实践,也有助于提升算法分析和设计能力。
本文将对10种经典的排序算法进行总结,并对每种算法的时间复杂度、空间复杂度和适用场景进行分析。 一、冒泡排序(Bubble Sort) 冒泡排序是一种简单的排序算法。它的工作原理是:从数组的第一个元素开始,比较...
7. `read_rnd_buffer_size`和`sort_buffer_size`: 分别用于随机读取和排序操作的缓冲区大小。 8. `join_buffer_size`: 处理JOIN操作时的缓冲区大小。 9. `table_cache`: 缓存打开的表对象的数量,减少打开和关闭表的...
根据提供的信息,我们可以总结出两个主要的编程面试题目及其解决方案,分别是求子数组的最大和以及查找最小的k个元素。 ### 1. 求子数组的最大和 #### 题目描述 输入一个整型数组,数组里包含正数和负数。要求找出...