堆的概念
在介紹堆排序之前,首先需要說明一下,堆是個什么玩意兒。
堆是一棵順序存儲的完全二叉樹。
其中每個結點的關鍵字都不大於其孩子結點的關鍵字,這樣的堆稱為小根堆。
其中每個結點的關鍵字都不小於其孩子結點的關鍵字,這樣的堆稱為大根堆。
舉例來說,對於n個元素的序列{R0, R1, ... , Rn}當且僅當滿足下列關系之一時,稱之為堆:
(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;
如上圖所示,序列R{3, 8, 15, 31, 25}是一個典型的小根堆。
堆中有兩個父結點,元素3和元素8。
元素3在數組中以R[0]表示,它的左孩子結點是R[1],右孩子結點是R[2]。
元素8在數組中以R[1]表示,它的左孩子結點是R[3],右孩子結點是R[4],它的父結點是R[0]。可以看出,它們滿足以下規律:
設當前元素在數組中以R[i]表示,那么,
(1) 它的左孩子結點是:R[2*i+1];
(2) 它的右孩子結點是:R[2*i+2];
(3) 它的父結點是:R[(i-1)/2];
(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。
要點
首先,按堆的定義將數組R[0..n]調整為堆(這個過程稱為創建初始堆),交換R[0]和R[n];
然后,將R[0..n-1]調整為堆,交換R[0]和R[n-1];
如此反復,直到交換了R[0]和R[1]為止。
以上思想可歸納為兩個操作:
(1)根據初始數組去構造初始堆(構建一個完全二叉樹,保證所有的父結點都比它的孩子結點數值大)。
(2)每次交換第一個和最后一個元素,輸出最后一個元素(最大值),然后把剩下元素重新調整為大根堆。
當輸出完最后一個元素后,這個數組已經是按照從小到大的順序排列了。
先通過詳細的實例圖來看一下,如何構建初始堆。
設有一個無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
構造了初始堆后,我們來看一下完整的堆排序處理:
還是針對前面提到的無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 來加以說明。
相信,通過以上兩幅圖,應該能很直觀的演示堆排序的操作處理。
核心代碼
int temp = array[parent]; // temp保存當前父節點
int child = 2 * parent + 1; // 先獲得左孩子
while (child < length) {
// 如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點
if (child + 1 < length && array[child] < array[child + 1]) {
child++;
}
// 如果父結點的值已經大於孩子結點的值,則直接結束
if (temp >= array[child])
break;
// 把孩子結點的值賦給父結點
array[parent] = array[child];
// 選取孩子結點的左孩子結點,繼續向下篩選
parent = child;
child = 2 * child + 1;
}
array[parent] = temp;
}
public void heapSort( int[] list) {
// 循環建立初始堆
for ( int i = list.length / 2; i >= 0; i--) {
HeapAdjust(list, i, list.length);
}
// 進行n-1次循環,完成排序
for ( int i = list.length - 1; i > 0; i--) {
// 最后一個元素和第一元素進行交換
int temp = list[i];
list[i] = list[0];
list[0] = temp;
// 篩選 R[0] 結點,得到i-1個結點的堆
HeapAdjust(list, 0, i);
System.out.format("第 %d 趟: \t", list.length - i);
printPart(list, 0, list.length - 1);
}
}
算法分析
堆排序算法的總體情況
排序類別 |
排序方法 |
時間復雜度 |
空間復雜度 |
穩定性 |
復雜性 |
||
平均情況 |
最壞情況 |
最好情況 |
|||||
選擇排序 |
堆排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(1) |
不穩定 |
較復雜 |
時間復雜度
堆的存儲表示是順序的。因為堆所對應的二叉樹為完全二叉樹,而完全二叉樹通常采用順序存儲方式。
當想得到一個序列中第k個最小的元素之前的部分排序序列,最好采用堆排序。
因為堆排序的時間復雜度是O(n+klog2n),若k≤n/log2n,則可得到的時間復雜度為O(n)。
算法穩定性
堆排序是一種不穩定的排序方法。
因為在堆的調整過程中,關鍵字進行比較和交換所走的是該結點到葉子結點的一條路徑,
因此對於相同的關鍵字就可能出現排在后面的關鍵字被交換到前面來的情況。
完整參考代碼
JAVA版本
代碼實現
以下范例是對上文提到的無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 進行排序。
public class HeapSort { public void HeapAdjust(int[] array, int parent, int length) { int temp = array[parent]; // temp保存當前父節點 int child = 2 * parent + 1; // 先獲得左孩子 while (child < length) { // 如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點 if (child + 1 < length && array[child] < array[child + 1]) { child++; } // 如果父結點的值已經大於孩子結點的值,則直接結束 if (temp >= array[child]) break; // 把孩子結點的值賦給父結點 array[parent] = array[child]; // 選取孩子結點的左孩子結點,繼續向下篩選 parent = child; child = 2 * child + 1; } array[parent] = temp; } public void heapSort(int[] list) { // 循環建立初始堆 for (int i = list.length / 2; i >= 0; i--) { HeapAdjust(list, i, list.length); } // 進行n-1次循環,完成排序 for (int i = list.length - 1; i > 0; i--) { // 最后一個元素和第一元素進行交換 int temp = list[i]; list[i] = list[0]; list[0] = temp; // 篩選 R[0] 結點,得到i-1個結點的堆 HeapAdjust(list, 0, i); System.out.format("第 %d 趟: \t", list.length - i); printPart(list, 0, list.length - 1); } } // 打印序列 public void printPart(int[] list, int begin, int end) { for (int i = 0; i < begin; i++) { System.out.print("\t"); } for (int i = begin; i <= end; i++) { System.out.print(list[i] + "\t"); } System.out.println(); } public static void main(String[] args) { // 初始化一個序列 int[] array = { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }; // 調用快速排序方法 HeapSort heap = new HeapSort(); System.out.print("排序前:\t"); heap.printPart(array, 0, array.length - 1); heap.heapSort(array); System.out.print("排序后:\t"); heap.printPart(array, 0, array.length - 1); } }
運行結果
第 1 趟: 8 7 6 5 2 1 4 3 0 9
第 2 趟: 7 5 6 3 2 1 4 0 8 9
第 3 趟: 6 5 4 3 2 1 0 7 8 9
第 4 趟: 5 3 4 0 2 1 6 7 8 9
第 5 趟: 4 3 1 0 2 5 6 7 8 9
第 6 趟: 3 2 1 0 4 5 6 7 8 9
第 7 趟: 2 0 1 3 4 5 6 7 8 9
第 8 趟: 1 0 2 3 4 5 6 7 8 9
第 9 趟: 0 1 2 3 4 5 6 7 8 9
排序后: 0 1 2 3 4 5 6 7 8 9
參考資料
《數據結構習題與解析》(B級第3版)
相關閱讀
歡迎閱讀 程序員的內功——算法 系列
示例源碼:https://github.com/dunwu/algorithm-notes
說明
感謝 @放學路上的小學生 的指正,他提到的第一種修改方法是有效的。本文已修改。
在我的 github 中,提供了單元測試來進行排序驗證:
樣本包含:數組個數為奇數、偶數的情況;元素重復或不重復的情況。且樣本均為隨機樣本,實測有效。