堆排序引入了另外一種演算法設計技術:利用某種資料結構(在此演算法中為“堆”)來管理演算法執行中的資訊。
一、堆堆簡介
我們通常使用的堆的二元堆積,它是一種數組對象,可以被視為一棵完全二叉樹。樹中的每個節點與數組中的節點相對應。如所示:
表示堆的數組通常由兩個屬性:數組中元素的個數length[A],存放在A中的堆的元素的個數heap-size[A]。也就是說存放在A中的一些元素可能不屬於對應的堆。因此:
heap-size[A] <= length[A]。
給定了某個節點的下標i,其父節點、左兒子以及右兒子可以很容易計算出來:
注意:下標均以1開始,而不是0
父節點:PARENT(i) = floor(i / 2)(向下取整)
左兒子:LEFT(i) = 2 * i
右兒子:RIGHT(i) = 2 * i + 1
堆的分類
二元堆積通常分為大根堆和小根堆。
在大根堆中,對於以某個節點為根的子樹,其各節點的值都不大於其根節點的值,即A[PARENT(i)] >= A[i]
小根堆則正好相反。
在堆排序中我們使用的是大根堆(從小到大排列)。小根堆通常在STL優先順序隊列中使用。
堆的高度
節點在堆中的高度被定義為此節點到葉子的最長簡單下降路徑中邊的數目。
堆的高度即根節點的高度。
二、堆的調整
很多時候,一棵二叉樹不滿足大根堆的性質,我們需要採用某種演算法進行調整以使其變為大根堆。下面的函數MaxHeapify將會實現此功能。我們假定以某個節點i的左兒子節點和右兒子節點為根的子樹都是大根堆,但是A[i]可能小於其子節點的值,這樣就違背了大根堆的性質。
演算法大致思想:
首先找出i節點和其左右子節點共3個節點中值最大的節點,如果不是i,則將i與值最大的節點互換。這樣確保了根i處的值是最大的。然後調整以剛才與i互換的子節點為根的子樹,遞迴調用演算法MaxHeapify。
演算法CPP代碼實現:
//得到父節點索引int getParent(int i){return (int)floor((float)i / 2);}//得到左子樹索引int getLeftSon(int i){return (2 * i);}//得到右子樹索引int getRightSon(int i){return (2 * i + 1);}//調整以某個節點i為根節點的子樹為大根堆void MaxHeapify(int A[],int i,int HeapSize){int left = getLeftSon(i);int right = getRightSon(i);int largest = i;//記錄值最大的元素的索引if (left <= HeapSize && A[left] > A[i]){largest = left;}if (right <= HeapSize && A[right] > A[largest]){largest = right;}if (largest != i)//此子樹不滿足大根堆的性質,需要進行調整{//進行交換int temp = A[i];A[i] = A[largest];A[largest] = temp;MaxHeapify(A,largest,HeapSize);//遞迴調用,繼續調整子樹}}
調整堆樣本
時間複雜度的分析:
調整A[i]、A[left]、A[right]的時間為常量時間。
下面分析遞迴調整以i的某個子節點為根的子樹所需的時間。
假設樹的節點個數為n,則i節點的子樹節點個數最多為2n/3(在最底層剛好半滿的時候,由完全二叉樹性質可得),
推導過程:
樹總的節點個數
n = pow(2,0) + pow(2,1) + ... + pow(2,h - 1 - 1) + 1 / 2 * pow(2,h - 1),其中h為樹的高度(根為第一層)
= 3 * pow(2,h - 2) - 1
假設根節點的左兒子所對應的子樹節點數大於右子樹的節點,其高度應為h - 1,節點數:
pow(2,0) + pow(2,1) + ... + pow(2,h - 1 - 1) = pow(2,h - 1) - 1
結合上面的式子可達到,子樹的最大節點數為(2 * n - 1) / 3.
這樣,調整堆的時間複雜度的推算為:
T(n) <= T(2n / 3) + O(1)
求解遞迴式得到:T(n) = O(lgn)。或者是O(h),h為樹的高度。
三、建堆建堆思想
我們可以自底向上地調用上述調整堆的方法MaxHeapify來將一個數組變成最大堆。
注意:子數組A[floor(n/2) + 1]...A[n]是樹的葉子。顯然葉子可以看成只有一個元素的大根堆,不用調整。只需從非葉子節點自後向前依次調整即可。
建堆樣本
為建堆的一個樣本:
最後一個非分葉節點是2,因此從2開始往前依次調用調整函數。
C++代碼實現:
//建堆void buildMaxHeap(int A[],int HeapSize){for (int i = (int)floor((float)HeapSize / 2);i > 0;--i){MaxHeapify(A,i,HeapSize);}cout << "建成的大根堆:" << endl;printHeap(A,HeapSize);}
時間複雜度分析
一個有n個元素堆的高度為floor(lgn),並且在任意高度h上之多有ceil(n/pow(2,h+1))個節點。
這樣,時間複雜度推算為:
而右邊的和式可以如下計算:
由式子:
兩端求導並乘以x可得:
,因此有:
於是,建堆的時間複雜度:
即可以線上性時間內將一個無序數組建成大根堆。
四、堆排序演算法思想
首先是將無序數組建成大根堆,然後通過把最大元素即根與A[n]互換使得最大元素到達正確位置。
然後將節點n從堆中去掉,原來根的子女仍然是大根堆,但是新的根項目可能違反了大根堆的規則,必須重新調整A[1,...,n - 1]為大根堆。這樣重複進行。直至堆的大小變為1才結束。
堆排序樣本
C++代碼實現:
//堆排序void heapSort(int A[],int HeapSize){buildMaxHeap(A,HeapSize);for (int i = HeapSize;i > 0;--i){int temp = A[1];A[1] = A[i];A[i] = temp;MaxHeapify(A,1,i - 1);}}
時間複雜度分析
堆排序的時間複雜度是O(nlgn)。其中建堆的時間是O(n)。n-1次調整堆,每次時間代價是O(logn)。
而且是一種原地排序演算法,即在任何時刻數組中只有常數個元素儲存在輸入數組以外。
程式執行個體:
#include <iostream>#include <cmath>using namespace std;//注意:下表都以1開始,而不是0//得到父節點索引int getParent(int i){return i>>1;}//得到左子樹索引int getLeftSon(int i){return i<<1;}//得到右子樹索引int getRightSon(int i){return ((i<<1) + 1);}//調整以某個節點i為根節點的子樹為大根堆void MaxHeapify(int A[],int i,int HeapSize){int left = getLeftSon(i);int right = getRightSon(i);int largest = i;//記錄值最大的元素的索引if (left <= HeapSize && A[left] > A[i]){largest = left;}if (right <= HeapSize && A[right] > A[largest]){largest = right;}if (largest != i)//此子樹不滿足大根堆的性質,需要進行調整{//進行交換int temp = A[i];A[i] = A[largest];A[largest] = temp;MaxHeapify(A,largest,HeapSize);//遞迴調用,繼續調整子樹}}//輸出數組元素void printHeap(int A[],int HeapSize){for(int i = 1;i <= HeapSize;++i){cout << A[i] << " ";}cout << endl;}//建堆void buildMaxHeap(int A[],int HeapSize){for (int i = (int)floor((float)HeapSize / 2);i > 0;--i){MaxHeapify(A,i,HeapSize);}cout << "建成的大根堆:" << endl;printHeap(A,HeapSize);}//堆排序void heapSort(int A[],int HeapSize){buildMaxHeap(A,HeapSize);for (int i = HeapSize;i > 0;--i){int temp = A[1];A[1] = A[i];A[i] = temp;MaxHeapify(A,1,i - 1);}}int main(){const int length = 11;//堆元素下班從1開始int A[length] = {0,4,1,3,2,16,9,10,14,8,7};int HeapSize = length - 1;heapSort(A,HeapSize);cout << "堆排序之後:" << endl;printHeap(A,HeapSize);}
運行結果:
五、堆排序的應用
堆排序應用廣泛:
(1)STL中優先順序隊列。
(2)尋找第K大的數或者最大的K個數。具體可以參考《編程之美》。
比如海量資料處理中,從1億個數中找出最大的1000個。
在構建哈夫曼樹時,也可以使用這種策略來提高效率。我們需要每次從集合中選擇兩個最小的元素,然後將元素相加,合并為一個新的元素。我們可以堆來提高效率。