前言
在我的前一篇文章里已经对堆排序有了一个详细的介绍。以最大堆为例,我们实现的buildMaxHeap的方法是在将所有元素放置到一个数组中再按照maxHeapify的子流程进行调整。这里有一个前提条件就是所有的数据已经就位了。在另外一些情况下,如果数据没有完全就位,比如说要从外部的数据源读,一次读一个数据进来,我们可以采用另外一种方式来建堆,也就是maxHeapInsert方法。在这里,我们对两种建堆的方法进行详细的分析和比较。
buildMaxHeap方法
buildMaxHeap方法的流程简单概括起来就是一句话,从A.length / 2一直到根结点进行maxHeapify调整。在前面的文章中对该流程有过详细的解读,这里就只是贴出来一个详细的实现:
public static void maxHeapify(int[] a, int i, int length) { int l = left(i); int r = right(i); int largest = i; while(true) { if(l < length && a[l] > a[i]) largest = l; if(r < length && a[r] > a[largest]) largest = r; if(i != largest) swap(a, i, largest); else break; i = largest; l = left(largest); r = right(largest); } } public static void buildMaxHeap(int[] a) { for(int i = a.length / 2; i >= 0; i--) maxHeapify(a, i, a.length); }
运行时间分析
粗粗来看前面buildmaxheap的时间复杂度,每次maxHeapify调整需要的时间为lg(n), 总共要遍历的元素有N/2个,所以大致的运行时间复杂度为O(nlgn).
如果我们更进一步分析,会发现它的实际情况会更理想一点。首先一个,我们从第a.length/2个元素开始执行maxHeapify,最开始这一层的元素只有一个子结点,也就是说,就算要调整,顶多一次就搞定了,不需要走lgn这么多步。
要做进一步的分析,我们可以先思考一下我们要建的这个完全二叉树堆的几个特性。以如下图为例:
我们看这棵二叉树,它必须保证每一层填满之后才能去填充下一层。而且,如果从根结点开始计数,往下第i层的元素如果不是最后一层的话,这一层的元素数量为2**i(2的i次方)。这样,对于一棵高为h的二叉树,它的所有结点数目就等于前面完全填满的层元素加上最下面一层的元素。
为什么要把他们分开来计数呢?是因为最下面一层的元素有一个变动的范围,作为一棵高度为h的树,最下面一层的元素最少可以是1,最大可以是把整层填充满,也就是2**(h+1)。这样,他们求和的结果就是最少为2**h,最大为2**(h+1)。
所以假设堆的元素数量为n的话,我们就可以推出:
结合这一步分析,我们可以得到: h <= lgn < h + 1。
结论1:
我们可以发现一个n个元素的树,它的高度相当于logn(向下取整)。
我们再来看我们分析的第二个结论。对应树每一个高度的一层,该有多少个元素呢?假设高度为1的那一层他们的元素个数为k,那么他们的访问时间复杂度为O(k)。根据前面的分析,我们还发现一个情况,就是如果从根结点开始计数,往下第i层的元素如果不是最后一层的话,这一层的元素数量为2**i(2的i次方)。正好这个第i层就相当于树的总高度减去当前层的结点的高度。假设第i层的高度为h,那么也就是i = lgn - h。
结论2:
这一层的元素数量为:
那么他们所有元素的运算时间总和为如下:
根据如下公式:
则有:
现在,我们发现,buildMaxHeap方法的时间复杂度实际上为O(n).
maxHeapInsert方法
maxHeapInsert方法和前面的办法不一样。它可以假定我们事先不知道有多少个元素,通过不断往堆里面插入元素进行调整来构建堆。
它的大致步骤如下:
1. 首先增加堆的长度,在最末尾的地方加入最新插入的元素。
2. 比较当前元素和它的父结点值,如果比父结点值大,则交换两个元素,否则返回。
3. 重复步骤2.
这个过程对应的代码实现如下:
public void heapIncreaseKey(int i, int key) throws Exception { if(key < a[i]) throw new Exception("new key is small than current key"); a[i] = key; while(i > 0 && a[parent(i)] < a[i]) { swap(i, parent(i)); i = parent(i); } } public void maxHeapInsert(int key) throws Exception { heapSize++; a[heapSize - 1] = Integer.MIN_VALUE; heapIncreaseKey(heapSize - 1, key); }
这里的parent()方法是用来求当前结点的父结点。详细的实现可以参考后面附件里的代码。
这里,我们也可以分析一下插入建堆的时间复杂度。我们先看最理想的情况,假设每次插入的元素都是严格递减的,那么每个元素只需要和它的父结点比较一次。那么其最优情况就是n。
对于最坏的情况下,每次新增加一个元素都需要调整到它的根结点。而这个长度为lgn。那么它的时间复杂度为如下公式:
这样,插入建堆的时间复杂度为nlgn。
总结
常用的建堆方法主要用于堆元素已经确定好的情况,而插入建堆的过程主要用于动态的增加元素来建堆。插入建堆的过程也常用于建立优先队列的应用。这些可以根据具体的时间情况来选取。
相关推荐
堆排序是一种基于比较的排序算法,它通过构造一个大顶堆或小顶堆来实现排序。在本场景中,我们关注的是堆排序的源代码,它适用于openSUSE 11.4操作系统,并且是使用GCC version 4.5.1编译器编译的。在这个名为"sort...
堆排序的实现需要解决两个问题:如何由一个无序序列建成一个堆?如何在输出堆顶元素后,调整剩余元素为一个新的堆? 堆的调整:堆的调整是指在输出堆顶元素后,调整剩余元素为一个新的堆的过程。解决方法是:输出堆...
堆排序分为建堆和调整堆两步,建堆将无序数组构造成一个大顶堆(或小顶堆),然后将堆顶元素与末尾元素交换,缩小排序范围,重复此过程直到排序完成。堆排序的时间复杂度为O(n log n),并且是原地排序,不需要额外的...
总结来说,堆排序是一种利用堆数据结构进行排序的有效方法,它包括了建堆和调整堆两个关键步骤。在C++中,堆排序可以通过自定义函数实现,如`heapify()`、`buildHeap()`和`heapSort()`。理解并实践这些函数的编写,...
堆排序是一种基于比较的排序算法,它通过构建和调整二叉堆来实现数据的排序。在二叉堆中,每个父节点的值都大于或等于其子节点的值,这样的堆被称为最大堆。堆排序的基本步骤包括建堆、交换根节点与最后一个元素、...
在ACM(国际大学生程序设计竞赛)中,堆排序是一种常用且高效的排序算法,对于解决时间限制严格的在线问题尤其有用。本篇文章将深入探讨堆排序的原理、实现以及如何将其应用到ACM竞赛中。 首先,堆是一个近似完全...
堆排序是一种基于比较的排序算法,它通过构造一个大顶堆或小顶堆来实现排序。在计算机科学中,堆通常被理解为一种特殊的完全二叉树,其中每个父节点的值都大于或等于(对于大顶堆)或小于或等于(对于小顶堆)其子...
堆排序分为建堆和调整堆两个阶段,首先将无序序列构造成一个大顶堆或小顶堆,然后将堆顶元素与末尾元素交换,调整剩下的元素为新的堆,如此反复进行,直到所有元素排序完毕。堆排序是一种不稳定的排序方法,但效率...
5. **堆排序**:利用堆这种数据结构实现排序,分为建堆和调整堆两步。C#中,可以使用System.Collections.Generic.PriorityQueue类辅助实现。堆排序在最坏、最好和平均情况下的时间复杂度都是O(n log n)。 6. **二路...
堆排序是一种基于比较的排序算法,它通过构建和调整二叉堆来实现数据的排序。在C++中,我们可以利用数组来模拟堆的数据结构。以下将详细介绍堆排序的原理、步骤以及如何用C++实现。 堆排序的核心在于堆数据结构。一...
堆排序是一种基于比较的排序算法,它通过构造一个大顶堆或小顶堆来实现排序。在C++中实现堆排序,我们需要理解堆的概念、如何构建堆以及如何维护堆的性质。下面将详细介绍堆排序的基本原理、C++实现步骤以及在VC6.0...
【堆排序】是一种基于比较的排序算法,它利用了数据结构中的“堆”这一概念。在计算机科学中,堆通常被视为一种特殊的完全二叉树,分为两种类型:最大堆和最小堆。最大堆中每个父节点的键值都大于或等于其子节点,而...
堆排序是一种高效的比较类排序算法,它利用了二叉堆的数据结构来实现排序。堆排序可以分为两个主要步骤:建堆(将无序的元素构建成一个堆)和排序(利用堆的性质进行排序)。 代码首先定义了一个HeapSort类,其中...
堆排序可以分为两个主要步骤:建堆(将无序的元素构建成一个堆)和排序(利用堆的性质进行排序)。 代码首先定义了一个HeapSort类,其中包含了sort方法用于执行堆排序,heapify方法用于维护堆的性质。main方法用于...
堆排序分为建堆和调整堆两步,时间复杂度为O(n log n),且是原地排序,无需额外存储空间。 这五种排序算法各有优缺点,适用场景不同。快速排序在大多数情况下表现优秀,但不适用于数据量小且已部分排序的情况;归并...
最小堆排序的过程分为两个阶段:建堆和堆排序。在建堆阶段,我们首先将无序数组构造成一个最小堆。这个过程从最后一个非叶子节点开始,向前遍历并调整每个节点,确保它们满足堆的性质。一旦建成最小堆,我们可以开始...
堆排序是一种基于比较的排序算法,它利用了数据结构中的“堆”这一概念。在Java中,我们可以使用数组来表示一个二叉堆。堆排序分为两个主要步骤:建堆和调整堆。 首先,我们理解一下什么是堆。堆是一个近似完全...
堆排序是一种高效的排序算法,基于比较的内部排序方法,它利用了数据结构中的“堆”这一概念。在C语言中实现堆排序,首先需要理解堆的性质:堆是一个完全二叉树,且满足堆的性质——父节点的值总是大于或等于(在最...
堆排序是一种基于比较的排序算法,它通过构建和操作一种特殊的数据结构——堆,来实现高效地对数据进行排序。堆是一种近似完全二叉树的结构,满足堆的性质:父节点的值总是大于或等于(在最大堆中)或小于或等于(在...