今天重温堆排序,在网上搜了好多博客文章,都是泛泛而谈。有的只讲了思路,有的直接贴上一份或几份代码。好一点的对复杂度进行了分析,但是讲到建堆复杂度,就一笔带过或者说请参考算法导论××页。我觉得求建堆复杂度并不难,了解一下对于理解堆排序是有好处的,下文为求解过程。
堆排序就是借助于堆的数据结构和堆的操作函数来完成排序功能的过程。堆的数据结构可以借助于数组表示出来并可以高效地进行堆的操作。我们为堆(最大堆)的元素从从上到下(从根到叶),从左到右进行1到n的编号,对应到数组的相应Index。为了方便对应,这里数组的0位置空了出来。定义几个操作:
以上三个操作的意义很简单,是属于堆操作中的函数。还有两个函数HEAPIFY , BUILD-HEAP,它们分别包装了一种特殊的堆修正操作和初始化建堆操作。其中HEAPIFY是在i的左右子树都是堆的前提下对以i为根的树进行修正堆的操作。
用以上几个堆的包装函数就可以完成堆排序函数HEAP-SORT。可以发现我们还需要分析一下HEAPIFY:BUILD-HEAP(int A[]){ heapsize = A.length-1; for(int i=headsize/2;i>=1;i--){ HEAPIFY(A,i); } }
HEAPIFY对一层的比对交换所需时间是常数级的O(1),然后进入递归过程。设堆共有N个节点,则高度最多为LgN,因此HEAPIFY最多递归LgN,耗费时间O(LgN)。HEAPIFY(int A[],int i){ int l = LEFT(i); int r = l + 1; int largest = i; if(l<=heapsize&&A[l]>A[i]){ largest = l; } if(r<=heapsize&&A[r]>A[largest]){ largest = r; } if(largest != i){ swap(i,largest); HEAPIFY(A,largest); } }
但是这并不是一个紧绷的复杂度,仔细想想也知道根本没进行(N/2)*LgN那么多次。
所有的叶节点都不进行HEAPIFY,HEAPIFY是从高度为1的节点开始进行直到根为止。这时候我们需要理解HEAPIFY的执行过程,而不能单纯的理解为LgN。对于高度为1的节点,至多替换发生1次。对于高度为2的节点,至多替换发生2次,以此类推,对于高度为h的节点,至多发生替换h次。我们知道,堆是满树,叶节点共有N/2个,它们的高度是0 。高度为1的节点正是他们的父节点,共有(N/2)/2个。高度为2的,类推有((N/2)/2)/2个。因此高度为h的共有N/(2的(h+1)次方)个。 好了,堆的高度总共只有0到LgN,现在每个高度的节点个数清楚,每个高度的每个节点至多发生的替换次数也清楚,则总共发生的替换数也就清楚了:(N/(2的(h+1)次方)) * h 的求和 (h取值0~LgN)
N是常数, 化一下变成(N/2) * ( h / (2的h次方) ) (h取值0~LgN)从上述推导过程可以看出,重点在于根据BUILD-HEAP过程找出计算复杂度的算式,然后利用求级数,求极限的方法解出结果。其实最终还是回归了理解算法和合理利用数学工具上。