关于堆排序建堆时间复杂度的证明

来源:互联网 发布:买家怎么撤销淘宝介入 编辑:程序博客网 时间:2024/06/06 16:41

建堆的过程,看起来外面一层循环O(n),里面是个logn的调整函数,时间复杂度貌似是nlogn的,但是仔细分析,其实质是O(n)的。

证明如下:

首先,对于高度为h的完全二叉树,其第i层的元素个数为2^(i-1),对于堆的每一层,调整的深度都不一样,每层的元素的调整深度小于等于h-i,假设每层调整的深度是h-i,欲构建的堆是个完全二叉树,那么对于每层来说:

最后一层不用调整;

倒数第二层的消耗是:2^(h-1)*1;

倒数第三层的消耗是:2^(h-2)*2;

。。。。。。

第一层的消耗是:2^(h-h)*(h-1);

加起来总消耗是:S=2^(h-1)*1+2^(h-2)*2+。。。+h;

2S=2^h*1+2^(h-1)*2+。。。+2*h;

S=2^h+2^(h-1)+2^(h-2)+。。。+2^1-h;

S=2^h+2^(h-1)+2^(h-2)+。。。+2^1+2^0-h-1;

S=2^(h+1)-2-h;

h=logn;

代入得:S=2*n-2-logn;

故堆排序的建堆过程是O(n)的。


代码:

[cpp] view plain copy
  1. template<class T> inline void MaxHeap<T>::make_heap(vector<T> & v) {  
  2.     if (heap_.size() != 0)  
  3.         heap_.clear();  
  4.     heap_ = v;  
  5.   
  6.     // start with last parent, and reheapify_down back to root  
  7.     for (int i = parent(v.size() - 1); i >= 0; i--)  
  8.         reheapify_down(i);  
  9.   
  10.     v = heap_;  
  11. }  


[cpp] view plain copy
  1. template<class T> inline void MaxHeap<T>::reheapify_down(int i) {  
  2.     if (heap_.empty())  
  3.         return;  
  4.   
  5.     int current = i, max = i;  
  6.     int left = 1, right = 2;  
  7.     int size = heap_.size();  
  8.   
  9.     do {  
  10.         current = max;  
  11.         left = left_child(current);  
  12.         right = right_child(current);  
  13.         if (left < size && heap_[left] > heap_[current])  
  14.             max = left;  
  15.         if (right < size && heap_[right] > heap_[max])  
  16.             max = right;  
  17.         if (max != current)  
  18.             swap(heap_[current], heap_[max]);  
  19.     } while (max != current);  
  20. }  

看似建堆(make_heap)的复杂度为O(nlogn),实则为O(n),reheapify_down不会touch到每个节点,所以不算简单的递归,只能算叠加。证明如下:


因为堆构建的是一颗平衡二叉树。于是有:

1. 一棵拥有n个节点的平衡二叉树,树高 h 约为 lg n 。

2. 树的第 i 层 最多有节点 2^i 个。注意,第 i 层的高度为 i + 1。如第0层的高度为1,拥有1个根节点。


那么做reheapify_down时,最底层(h-1)的节点不会动,倒数第二层(h-2)的节点最多往下移动一次,倒数第三层(h-3)的节点最多往下移动2次....第0层的节点往下最多移动(h-1)次。

所以,最坏情况下,每层所有节点都会往下移到最底层。

则,所有操作总和为     S = 2^(h-1)*0 + 2^(h-2)*1 + 2^(h-3) * 2 + ... + 2^1*(h-2) + 2^0*(h-1)        ----- (1)

把(1)式乘以2,再减去(1)式, 可得

S = 2^(h-1) + 2^(h-2) + ... + 2^1 - 2^0*(h-1)  = 2(1-2^(h-1))/(1-2) - (h-1) = 2^h - h- 1      ---- (2)

把h = lg n 代入 (2)式, 得 S = n - lgn - 1 <= n   (n >=1)


故而, 建堆复杂度为O(n) 。

================================================================================================================================

今天重温堆排序,在网上搜了好多博客文章,都是泛泛而谈。有的只讲了思路,有的直接贴上一份或几份代码。好一点的对复杂度进行了分析,但是讲到建堆复杂度,就一笔带过或者说请参考算法导论××页。我觉得求建堆复杂度并不难,了解一下对于理解堆排序是有好处的,下文为求解过程。

 

堆排序就是借助于堆的数据结构和堆的操作函数来完成排序功能的过程。堆的数据结构可以借助于数组表示出来并可以高效地进行堆的操作。我们为堆(最大堆)的元素从从上到下(从根到叶),从左到右进行1到n的编号,对应到数组的相应Index。为了方便对应,这里数组的0位置空了出来。定义几个操作:

 

 

  1. PARENT(i) : i/2 
  2. LEFT(i) : i<<1
  3. RIGHT(i) : (i<<1)+1

 

以上三个操作的意义很简单,是属于堆操作中的函数。还有两个函数HEAPIFY , BUILD-HEAP,它们分别包装了一种特殊的堆修正操作和初始化建堆操作。其中HEAPIFY是在i的左右子树都是堆的前提下对以i为根的树进行修正堆的操作。

 

用以上几个堆的包装函数就可以完成堆排序函数HEAP-SORT。
现在来分析建堆过程BUILD-HEAP:
[cpp] view plaincopy
  1. BUILD-HEAP(int A[]){  
  2.     heapsize A.length-1;  
  3.     for(int i=headsize/2;i>=1;i--){  
  4.         HEAPIFY(A,i);  
  5.      
  6.  
可以发现我们还需要分析一下HEAPIFY:
[cpp] view plaincopy
  1. HEAPIFY(int A[],int i){  
  2.     int LEFT(i);  
  3.     int 1;  
  4.     int largest i;  
  5.     if(l<=heapsize&&A[l]>A[i]){  
  6.         largest l;  
  7.      
  8.     if(r<=heapsize&&A[r]>A[largest]){  
  9.         largest r;  
  10.      
  11.     if(largest != i){  
  12.         swap(i,largest);  
  13.         HEAPIFY(A,largest);  
  14.      
  15.  

HEAPIFY对一层的比对交换所需时间是常数级的O(1),然后进入递归过程。设堆共有N个节点,则高度最多为LgN,因此HEAPIFY最多递归LgN,耗费时间O(LgN)。

再看BUILD-HEAP,循环HEAPIFY了N/2次,因此复杂度的上界很好理解,为(N/2)*LgN,即O(NLgN)。

但是这并不是一个紧绷的复杂度,仔细想想也知道根本没进行(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)
接下来就是一个级数求和问题了,学过高数的都应该知道怎么求。求 ( h / (2的h次方) ) (h取值0~LgN):
设结果为S,则S = 1/2 + 2/(2的2次方)  + 3/(2的3次方) ... + LgN/ (2的LgN次方)。另S*(1/2) = 1/(2的2次方) + 2/(2的3次方) + 3/(2的4次方)...+LgN/(2的(LgN+1)次方)。
两式错位相减 有 S*(1/2) = 1/2 + 1/(2的2次方)  + 1/(2的3次方) ... + 1/ (2的LgN次方) - LgN/(2的(LgN+1)次方)。
右式前边几项为等比数列,最终化简结果为S = 2 - (1/2)的(LgN-1)次方-LgN / ( 2的LgN次方)。
当N趋向于无穷大时,右式的二,三两项都趋近于0,于是limS = 2。所以我们要求的BUILD-HEAP复杂度为O( (N/2) * S ) = O(N)。

从上述推导过程可以看出,重点在于根据BUILD-HEAP过程找出计算复杂度的算式,然后利用求级数,求极限的方法解出结果。其实最终还是回归了理解算法和合理利用数学工具上。



0 0
原创粉丝点击