1.什麼是堆
這裏的堆(二叉堆),指得不是堆棧的那個堆,而是一種數據結構。
堆可以視爲一棵完全的二叉樹,完全二叉樹的一個「優秀」的性質是,除了最底層之外,每一層都是滿的,這使得堆可以利用數組來表示(普通的一般的二叉樹通常用鏈表作爲基本容器表示),每一個結點對應數組中的一個元素。
如下圖,是一個堆和數組的相互關係
二叉堆一般分爲兩種:最大堆和最小堆。兩種堆內部的數據都要滿足自己的特點。
比如最大堆的特點是,每個父節點的元素值都不小於其孩子結點(如果存在)的元素值,因此,最大堆的最大元素值出現在根結點(堆頂)
最小堆的性質與最大堆恰好相反
由於堆排序算法使用的是最大堆,所以我們這裏以最大堆爲例,最小堆情況類似,可以自己推導
對於給定的某個結點的下標i,可以很容易的計算出這個結點的父結點、孩子結點的下標,而且計算公式很漂亮很簡約
但是這裏有一個很大的問題:目前主流的編程語言中,數組都是Zero-based,這就意味着我們的堆數據結構模型要發生改變
相應的,幾個計算公式也要作出相應調整
新公式很難看,很杯具
這幾個公式在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)作用過程如圖所示
對於有n個元素的堆來說,MaxHeapify的運行時間最壞情況是O(logn)(可以通過主定理的得到)。而在事實上,這個複雜度和堆的高度成正比。我們可以證明,一個大小爲n的最大堆,他的高度是lowerbound(logn)
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 |
} |
排序的過程如圖所示
雖然BuildMaxHeap對於不同的初始數據排列所需要的時間不同,但是這並不影響HeapSort的總體時間複雜度
堆作爲數據結構,除了用於堆排序之外,更常見的用途是建立優先級隊列。
由於最大/最小元素出現在堆根本,所以很容易確定隊列元素的優先級。這也是堆最頻繁的用途