堆排序與快速排序,歸並排序一樣都是時間復雜度為O(N*logN)的幾種常見排序方法。學習堆排序前,先講解下什么是數據結構中的二叉堆。
堆的定義
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個元素的最小值(或最大值)。
堆的存儲
一般用數組來表示堆,若根結點存在序號0處, i結點的父結點下標就為(i-1)/2。i結點的左右子結點下標分別為2*i+1和2*i+2。
堆排序的實現
實現堆排序需要解決兩個問題:
1.如何由一個無序序列建成一個堆?
2.如何在輸出堆頂元素之后,調整剩余元素成為一個新的堆?
先考慮第二個問題,一般在輸出堆頂元素之后,視為將這個元素排除,然后用表中最后一個元素填補它的位置,自上向下進行調整:首先將堆頂元素和它的左右子樹的根結點進行比較,把最小的元素交換到堆頂;然后順着被破壞的路徑一路調整下去,直至葉子結點,就得到新的堆。
我們稱這個自堆頂至葉子的調整過程為“篩選”。
從無序序列建立堆的過程就是一個反復“篩選”的過程。
示例代碼:
package heapSort; /** * 堆排序,采用順序存儲 * 大根堆 * @author 超超boy * */ public class HeapSort2 { int[] arr; public static void main(String[] args) { // TODO Auto-generated method stub HeapSort2 heapSor = new HeapSort2(); int[] arr = {7,23,45,9,40,73,12,49}; //0下標放的是數組長度, heapSor.arr = arr; heapSor.heapSort(arr); for(int i=0;i<arr.length;i++) System.out.print(".."+arr[i]); } void heapAdjust(int[] arr,int s,int m){ //已知arr[s...m]中記錄的關鍵字除arr[s]之外均滿足堆的定義,本函數調整arr[s]的關鍵字,使arr[s...m]成為一個最大堆 int rc = arr[s]; //s是最后一個非葉子節點 for(int j=2*s;j <= m;j = s*2){ if(j<m && arr[j]<arr[j+1]) j++; //j為key較大的下標 if(rc >= arr[j]) break; arr[s] = arr[j]; //上移到父節點 s=j; } arr[s]=rc; //要放入的位置 } void heapSort(int[] arr){ //對數組進行建堆操作,就是從最后一個非葉結點進行篩選的過程 for(int i=(arr.length-1)/2;i > 0;i--){//因為0沒有使用,所以length-1 heapAdjust(arr,i,arr.length-1); } System.out.println("........建堆完成............."); outputArr(1); for(int i=arr.length-1; i>1; i--){ int temp = arr[1]; arr[1] = arr[i]; arr[i] = temp; heapAdjust(arr,1,i-1); } } void outputArr(int i){ if(i <= arr[0]){ System.out.println(arr[i]); outputArr(i*2); //左 outputArr(i*2+1); //右 } } }
運行結果:
.....................
73
45
40
23
49
12
9
..7..9..12..23..40..45..49..73
堆排序分析
堆排序方法對記錄數較少的文件並不值得提倡,但對n較大的文件還是很有效的。因為其運行時間主要耗費在建初始堆和調整建新堆時進行的反復“篩選”上。
堆排序在最壞的情況下,其時間復雜度也為O(nlogn)。相對於快速排序來說,這是堆排序的最大優點。此外,堆排序僅需一個記錄大小的供交換用的輔助存儲空間。