堆排序 Heap Sort
堆排序是一種選擇排序,其時間復雜度為O(nlogn)。
堆的定義
n個元素的序列{k1,k2,…,kn}當且僅當滿足下列關系之一時,稱之為堆。
情形1:ki <= k2i 且ki <= k2i+1 (最小化堆或小頂堆)
情形2:ki >= k2i 且ki >= k2i+1 (最大化堆或大頂堆)
其中i=1,2,…,n/2向下取整;
若將和此序列對應的一維數組(即以一維數組作此序列的存儲結構)看成是一個完全二叉樹,則堆的含義表明,完全二叉樹中所有非終端結點的值均不大於(或不小於)其左、右孩子結點的值。
由此,若序列{k1,k2,…,kn}是堆,則堆頂元素(或完全二叉樹的根)必為序列中n個元素的最小值(或最大值)。
例如,下列兩個序列為堆,對應的完全二叉樹如圖:
若在輸出堆頂的最小值之后,使得剩余n-1個元素的序列重又建成一個堆,則得到n個元素的次小值。如此反復執行,便能得到一個有序序列,這個過程稱之為堆排序。
堆排序(Heap Sort)只需要一個記錄元素大小的輔助空間(供交換用),每個待排序的記錄僅占有一個存儲空間。
堆的存儲
一般用數組來表示堆,若根結點存在序號0處, i結點的父結點下標就為(i-1)/2。i結點的左右子結點下標分別為2*i+1和2*i+2。
(注:如果根結點是從1開始,則左右孩子結點分別是2i和2i+1。)
如第0個結點左右子結點下標分別為1和2。
如最大化堆如下:
左圖為其存儲結構,右圖為其邏輯結構。
堆排序的實現
實現堆排序需要解決兩個問題:
1.如何由一個無序序列建成一個堆?
2.如何在輸出堆頂元素之后,調整剩余元素成為一個新的堆?
先考慮第二個問題,一般在輸出堆頂元素之后,視為將這個元素排除,然后用表中最后一個元素填補它的位置,自上向下進行調整:首先將堆頂元素和它的左右子樹的根結點進行比較,把最小的元素交換到堆頂;然后順着被破壞的路徑一路調整下去,直至葉子結點,就得到新的堆。
我們稱這個自堆頂至葉子的調整過程為“篩選”。
從無序序列建立堆的過程就是一個反復“篩選”的過程。
構造初始堆
初始化堆的時候是對所有的非葉子結點進行篩選。
最后一個非終端元素的下標是[n/2]向下取整,所以篩選只需要從第[n/2]向下取整個元素開始,從后往前進行調整。
比如,給定一個數組,首先根據該數組元素構造一個完全二叉樹。
然后從最后一個非葉子結點開始,每次都是從父結點、左孩子、右孩子中進行比較交換,交換可能會引起孩子結點不滿足堆的性質,所以每次交換之后需要重新對被交換的孩子結點進行調整。
進行堆排序
有了初始堆之后就可以進行排序了。
堆排序是一種選擇排序。建立的初始堆為初始的無序區。
排序開始,首先輸出堆頂元素(因為它是最值),將堆頂元素和最后一個元素交換,這樣,第n個位置(即最后一個位置)作為有序區,前n-1個位置仍是無序區,對無序區進行調整,得到堆之后,再交換堆頂和最后一個元素,這樣有序區長度變為2。。。
不斷進行此操作,將剩下的元素重新調整為堆,然后輸出堆頂元素到有序區。每次交換都導致無序區-1,有序區+1。不斷重復此過程直到有序區長度增長為n-1,排序完成。
堆排序實例
首先,建立初始的堆結構如圖:
然后,交換堆頂的元素和最后一個元素,此時最后一個位置作為有序區(有序區顯示為黃色),然后進行其他無序區的堆調整,重新得到大頂堆后,交換堆頂和倒數第二個元素的位置……
重復此過程:
最后,有序區擴展完成即排序完成:
由排序過程可見,若想得到升序,則建立大頂堆,若想得到降序,則建立小頂堆。
代碼
假設排列的元素為整型,且元素的關鍵字為其本身。
因為要進行升序排列,所以用大頂堆。
根結點從0開始,所以i結點的左右孩子結點的下標為2i+1和2i+2。

//堆篩選函數 //已知H[start~end]中除了start之外均滿足堆的定義 //本函數進行調整,使H[start~end]成為一個大頂堆 typedef int ElemType; void HeapAdjust(ElemType H[], int start, int end) { ElemType temp = H[start]; for(int i = 2*start + 1; i<=end; i*=2) { //因為假設根結點的序號為0而不是1,所以i結點左孩子和右孩子分別為2i+1和2i+2 if(i<end && H[i]<H[i+1])//左右孩子的比較 { ++i;//i為較大的記錄的下標 } if(temp > H[i])//左右孩子中獲勝者與父親的比較 { break; } //將孩子結點上位,則以孩子結點的位置進行下一輪的篩選 H[start]= H[i]; start = i; } H[start]= temp; //插入最開始不和諧的元素 } void HeapSort(ElemType A[], int n) { //先建立大頂堆 for(int i=n/2; i>=0; --i) { HeapAdjust(A,i,n); } //進行排序 for(int i=n-1; i>0; --i) { //最后一個元素和第一元素進行交換 ElemType temp=A[i]; A[i] = A[0]; A[0] = temp; //然后將剩下的無序元素繼續調整為大頂堆 HeapAdjust(A,0,i-1); } }
堆排序分析
堆排序方法對記錄數較少的文件並不值得提倡,但對n較大的文件還是很有效的。因為其運行時間主要耗費在建初始堆和調整建新堆時進行的反復“篩選”上。
堆排序在最壞的情況下,其時間復雜度也為O(nlogn)。相對於快速排序來說,這是堆排序的最大優點。此外,堆排序僅需一個記錄大小的供交換用的輔助存儲空間。
參考資料:
嚴蔚敏《數據結構》
http://www.cnblogs.com/dolphin0520/archive/2011/10/06/2199741.html
http://blog.csdn.net/morewindows/article/details/6709644