堆與堆排序

1.什麼是堆

這裏的堆(二叉堆),指得不是堆棧的那個堆,而是一種數據結構。

堆可以視爲一棵完全的二叉樹,完全二叉樹的一個「優秀」的性質是,除了最底層之外,每一層都是滿的,這使得堆可以利用數組來表示(普通的一般的二叉樹通常用鏈表作爲基本容器表示),每一個結點對應數組中的一個元素。

如下圖,是一個堆和數組的相互關係

Heap1.PNG

二叉堆一般分爲兩種:最大堆和最小堆。兩種堆內部的數據都要滿足自己的特點。

比如最大堆的特點是,每個父節點的元素值都不小於其孩子結點(如果存在)的元素值,因此,最大堆的最大元素值出現在根結點(堆頂)

最小堆的性質與最大堆恰好相反

由於堆排序算法使用的是最大堆,所以我們這裏以最大堆爲例,最小堆情況類似,可以自己推導

對於給定的某個結點的下標i,可以很容易的計算出這個結點的父結點、孩子結點的下標,而且計算公式很漂亮很簡約

gif.latex?%5C120dpi%20%20PARENT%5Cleft%20(%20i%20%5Cright%20)=%5Cleft%20%5Clfloor%20%5Cfrac%7Bi%7D%7B2%7D%20%5Cright%20%5Crfloor%20%5C%5C%20%5Cindent%20LEFT%5Cleft%20(%20i%20%5Cright%20)=2i%20%5C%5C%20%5Cindent%20RIGHT%5Cleft%20(%20i%20%5Cright%20)=2i+1

但是這裏有一個很大的問題:目前主流的編程語言中,數組都是Zero-based,這就意味着我們的堆數據結構模型要發生改變
 
Heap2.PNG

相應的,幾個計算公式也要作出相應調整

gif.latex?%5C120dpi%20PARENT%5Cleft%20(%20i%20%5Cright%20)=%5Cleft%20%5Clfloor%20%5Cfrac%7Bi+1%7D%7B2%7D%20%5Cright%20%5Crfloor%20-1%5C%5C%20%5Cindent%20LEFT%5Cleft%20(%20i%20%5Cright%20)=2i+1%20%5C%5C%20%5Cindent%20RIGHT%5Cleft%20(%20i%20%5Cright%20)=2%5Cleft%20(i+1%20%5Cright%20)

新公式很難看,很杯具

這幾個公式在C/C++中可以用宏或者內聯函數實現

1 #define LEFT(x) ((x << 1) + 1)
2 #define RIGHT(x) ((x + 1) << 1)
3 #define PARENT(x) (((x + 1) >> 1) - 1)

2.堆排序

堆排序是一種利用堆這種數據結構,進行原地排序的排序算法,其時間複雜度是O(nlogn),而且只和數據規模有關

堆排序算法是一種很漂亮的算法,這裏需要用到三個函數:MaxHeapify、BuildMaxHeap和HeapSort

2.1MaxHeapify

MaxHeapify的作用是保持最大堆的性質,是整個排序算法的核心。

MaxHeapify函數接受三個參數,數組,檢查的起始下標和堆大小。函數的代碼如下

01 /*
02     輸  入: Ary(int[]) - [in,out]排序數組
03             nIndex(int) - 起始下標
04             nHeapSize(int) - 堆大小(zero-based)
05     輸  出: -
06     功  能: 從nIndex開始檢查並保持最大堆性質
07 */
08 void MaxHeapify(int Ary[], int nIndex, int nHeapSize)
09 {
10     int nL = LEFT(nIndex);
11     int nR = RIGHT(nIndex);
12     int nLargest;
13   
14     if (nL <= nHeapSize && Ary[nIndex] < Ary[nL])
15     {
16         nLargest = nL;
17     }
18     else
19     {
20         nLargest = nIndex;
21     }
22   
23     if (nR <= nHeapSize && Ary[nLargest] < Ary[nR])
24     {
25         nLargest = nR;
26     }
27   
28     if (nLargest != nIndex)
29     {
30         // 調整後可能仍然違反堆性質
31         Swap(Ary[nLargest], Ary[nIndex]);
32         MaxHeapify(Ary, nLargest, nHeapSize);
33     }
34 }

由於一次調整後,堆仍然違反堆性質,所以需要遞歸的測試,使得整個堆都滿足堆性質

MaxHeapify(A,1,9)作用過程如圖所示

Heap3.png

對於有n個元素的堆來說,MaxHeapify的運行時間最壞情況是O(logn)(可以通過主定理的得到)。而在事實上,這個複雜度和堆的高度成正比。我們可以證明,一個大小爲n的最大堆,他的高度是lowerbound(logn)

gif.latex?%5C150dpi%20Suppose~the~height~of~Max~Heap~is~h%5C%5C%20%5Cindent%20So,~we~can~easily~draw~a~conclusion%5C%5C%20%5Cindent%20Maxinum~of~the~elements~is~%5Csum_%7Bk=0%7D%5E%7Bh%7D2%5E%7Bk%7D=2%5E%7Bh+1%7D-1%5C%5C%20%5Cindent%20Mininum~of~the~elements~is~%5Csum_%7Bk=0%7D%5E%7Bh-1%7D2%5E%7Bk%7D+1=2%5E%7Bh%7D%5C%5C%20%5Cindent%20obviously,~2%5E%7Bh%7D%5Cleq%20n%5Cleq%202%5E%7Bh+1%7D-1<%202%5E%7Bh+1%7D%5CRightarrow%20%5C%5C%20%5Cindent%20h%20%5Cleq%20logn<h+1%5CLeftrightarrow%20h=%5Cleft%20%5Clfloor%20logn%20%5Cright%20%5Crfloor

MaxHeapify很簡潔漂亮,但是由於遞歸的調用可能是某些編譯器產生「比較爛」的代碼。

通常來說,遞歸主要用在分治法中,而這裏並不需要分治。而且遞歸調用需要壓棧/清棧,和迭代相比,性能上有略微的劣勢。當然,按照20/80法則,這是可以忽略的。但是如果你覺得用遞歸會讓自己心裏過不去的話,也可以用迭代,比如下面醬紫

01 /*
02     輸  入: Ary(int[]) - [in,out]排序數組
03             nIndex(int) - 起始下標
04             nHeapSize(int) - 堆大小
05     輸  出: -
06     功  能: 從nIndex開始檢查並保持最大堆性質
07 */
08 void MaxHeapify(int Ary[], int nIndex, int nHeapSize)
09 {
10     while(true)
11     {
12         int nL = LEFT(nIndex);
13         int nR = RIGHT(nIndex);
14         int nLargest;
15   
16         if (nL <= nHeapSize && Ary[nIndex] < Ary[nL])
17         {
18             nLargest = nL;
19         }
20         else
21         {
22             nLargest = nIndex;
23         }
24   
25         if (nR <= nHeapSize && Ary[nLargest] < Ary[nR])
26         {
27             nLargest = nR;
28         }
29   
30         if (nLargest != nIndex)
31         {
32             // 調整後可能仍然違反堆性質
33             Swap(Ary[nLargest], Ary[nIndex]);
34             nIndex = nLargest;
35         }
36         else
37         {
38             break;
39         }
40     }
41 }

顯然沒有上個版本的漂亮- -

2.2BuildMaxHeap

BuildMaxHeap的作用是將一個數組改造成一個最大堆,接受數組和堆大小兩個參數

BuildMaxHeap中自下而上的調用MaxHeapify來改造數組,建立最大堆。因爲MaxHeapify能夠保證下標i的結點之後結點都滿足最大堆的性質,所以自下而上的調用MaxHeapify能夠在改造過程中保持這一性質。

如果最大堆的數量元素是n,那麼BuildMaxHeap從PARENT(n)開始,往上依次調用MaxHeapify。

這基於一個定理:如果最大堆有n個元素,那麼從PARENT(n)+1,PARENT(n)+2…n都是葉子結點(葉子結點指沒有兒子結點的結點)

BuildMaxHeap的代碼如下:

01 /*
02     輸  入: Ary(int[]) - [in,out]排序數組
03             nHeapSize(int) - [in]堆大小(zero-based)
04     輸  出: -
05     功  能: 將一個數組改造爲最大堆
06 */
07 void BuildMaxHeap(int Ary[], int nHeapSize)
08 {
09     for (int i = PARENT(nHeapSize); i >= 0; --i)
10     {
11         MaxHeapify(Ary, i, nHeapSize);
12     }
13 }

由於MaxHeapify的最壞情況是O(logn),所以BuildMaxHeap的最壞情況是O(nlogn),雖然這個複雜度是正確的(O給出複雜度的上界),但是不夠精確。

事實上,可以利用數學分析證明,BuildMaxHeap的期望複雜度是O(n)

而且,如果對一個遞減排列的數組來說,MaxHeapify的複雜度是O(1),BuildMaxHeap的複雜度也達到最優的O(n),cos一個遞減排列的數組本身滿足最大堆

2.3HeapSort

HeapSort是堆排序的接口算法,接受數組和元素個數兩個參數

HeapSort先調用BuildMaxHeap將數組改造爲最大堆,然後將堆頂和堆底元素交換,之後將底部上升,最後重新調用MaxHeapify保持最大堆性質。

由於堆頂元素必然是堆中最大的元素,所以一次操作之後,堆中存在的最大元素被分離出堆

重複n-1次之後,數組排列完畢。代碼如下

01 /*
02     輸  入: Ary(int[]) - [in,out]排序數組
03             nCount(int) - [in]元素個數
04     輸  出: -
05     功  能: 對一個數組進行堆排序
06 */
07 void HeapSort(int Ary[], int nCount)
08 {
09     int nHeapSize = nCount - 1;
10   
11     BuildMaxHeap(Ary, nHeapSize);
12   
13     for (int i = nHeapSize; i >= 1; --i)
14     {
15         Swap(Ary[0], Ary[i]);
16         --nHeapSize;
17         MaxHeapify(Ary, 0, nHeapSize);
18     }
19 }

排序的過程如圖所示

Heap4.png
Heap5.png
Heap6.png

雖然BuildMaxHeap對於不同的初始數據排列所需要的時間不同,但是這並不影響HeapSort的總體時間複雜度

堆作爲數據結構,除了用於堆排序之外,更常見的用途是建立優先級隊列。

由於最大/最小元素出現在堆根本,所以很容易確定隊列元素的優先級。這也是堆最頻繁的用途

 

http://blog.kingsamchen.com/archives/547#viewSource

轉載於:https://www.cnblogs.com/spirals/archive/2010/09/14/1825516.html