堆排序演算法及其Java實現(以大根堆為例)
(二叉)堆資料結構是一種陣列物件,如圖所示(下標從0開始),它完全可以被視為一棵完全二叉樹。
接下來要出現的幾個詞語,這裡介紹一下:
length[A]: 陣列A中元素的個數
heap-size[A]: 存放在陣列A中堆的元素的個數,是要排序的元素的個數,在進行堆排序時,這個是會變的(減1)
A[0]是樹的根,A[i]是陣列中的第i個元素(從0開始計數)
PARENT(i): 第i個元素父結點的位置,值為 (i - 1)/2
LEFT(i): 第i個元素左孩子的位置,值為2i + 1
RIGHT(i): 第i個元素右孩子的位置,值為 2i + 2
static int parent(int i) {
return (i - 1)/2;
}
static int left(int i) { //左孩子
return 2*i + 1;
}
static int right(int i) { //右孩子
return 2*i + 2;
}
操作:
MAX-HEAPIFY: 保持大根堆性質,執行時間O(lg n)
BUILD-MAX-HEAP: 大根堆初始建堆,即在無序的輸入基礎上建堆,執行時間O(n)
HEAPSORT: 堆排序,對一個數組進行原地排序,執行時間O(n*lg n)
一、MAX-HEAPIFY(保持大根堆性質)
大根堆的性質:A[PARENT(i)] >= A[i],MAX-HEAPIFY是對大根堆進行操作的重要子程式,後面要經常用到。
輸入:以a[i]為根結點的子樹,且以a的左孩子和a的右孩子為根節點的子樹都滿足大根堆性質
輸出:樹形沒有變,但結點順序可能發生改變的子樹(該樹的所有子樹的根節點的值都大於它左右孩子的值,保持了大根堆性質)
static void maxHeapfy(int []a,int i,int heapSize) { //陣列a,第i個結點,heapSize是陣列中實際要排序的元素的長度
int left = left(i); //有的時候能夠遞迴到葉子結點,葉子結點無後繼,下面兩個if都注意到了這一點
int right = right(i);
int largest = i;
if(left < heapSize && a[left] > a[largest]) { //
largest = left;
}
if (right < heapSize && a[right] > a[largest])
{
largest = right;
}
if(largest != i) { //把最大值給父結點
a[largest] = a[largest] ^ a[i];
a[i] = a[largest] ^ a[i];
a[largest] = a[largest] ^ a[i];
maxHeapfy(a,largest,heapSize); //發生交換之後還要保證大根堆性質
}
}
二、BUILD-MAX-HEAP(初始建堆)
輸入:無序的一組數
輸出:一組數,按層序對應一棵完全二叉樹,並且在它的每個子樹中,根節點的值都大於其後代結點的值
步驟:自底向上,從最後一個非葉子結點開始呼叫MAX-HEAPFY操作,以下圖為例
程式碼實現:
static void buildMaxHeap(int []a,int heapSize) {
for(int i = (heapSize-1)/2;i >= 0;i--) {
maxHeapfy(a,i,heapSize);
}
}
三、堆排序(HEAPSORT)
通過初始建堆,陣列中最大的元素在A[0],此時我們把A[0]與A[n-1]互換,對新陣列A[0]~A[n-2]重新建堆,然後第二大的元素又落在了A[0]的位置上,此時我們把A[0]與A[n-2]互換…..以此類推,堆的大小由n-1一直降到2,我們得到原輸入序列的升序排列
程式碼:
static void heapSort(int []a) {
for(int i = a.length-1;i > 0;i--) {
buildMaxHeap(a,i+1); //堆的大小從n到2
a[i] = a[0] ^ a[i]; //交換
a[0] = a[0] ^ a[i];
a[i] = a[0] ^ a[i];
}
}
在初始建堆之後,原根結點的左右子樹均滿足最大堆的性質,所以實際上在對A[0]~A[n-2]進行初始建堆的時候,只有在根結點A[0]處執行了MAX-HEAPIFY操作,下面的程式碼換一種更直觀的寫法:
static void heapSort(int []a) {
int len = a.length;
buildMaxHeap(a,len); //初始建堆
a[len-1] = a[0] ^ a[len-1]; //交換
a[0] = a[0] ^ a[a.length-1];
a[len-1] = a[0] ^ a[len-1];
for(int i = 1;i<len-1;i++) { //初始建堆之後還要排a.length-2次
maxHeapfy(a,0,len-i);
a[len-1-i] = a[0] ^ a[len-1-i]; //交換
a[0] = a[0] ^ a[len-1-i];
a[len-1-i] = a[0] ^ a[len-1-i];
}
}
//如有錯誤,歡迎指正
整個程式程式碼如下:
package ex;
import java.util.Arrays;
public class Sort {
public static void main(String args[]) {
int []a = new int[] {16,25,34,27,30,5,7,4,41,55};
Sort.heapSort(a);
System.out.println(Arrays.toString(a));
}
static int parent(int i) {
return (i - 1)/2;
}
static int left(int i) {
return 2*i + 1;
}
static int right(int i) {
return 2*i + 2;
}
static void maxHeapfy(int []a,int i,int heapSize) { //陣列a,第i個結點,heapSize是陣列種實際要排序的元素的長度
int left = left(i); //有的時候能夠遞迴到葉子結點,葉子結點無後繼,下面兩個if都注意到了這一點
int right = right(i);
int largest = i;
if(left < heapSize && a[left] > a[largest]) { //
largest = left;
}
if(right < heapSize && a[right] > a[largest])
{
largest = right;
}
if(largest != i) { //把最大值給父結點
a[largest] = a[largest] ^ a[i];
a[i] = a[largest] ^ a[i];
a[largest] = a[largest] ^ a[i];
maxHeapfy(a,largest,heapSize); //發生交換之後還要保證大根堆性質
}
}
static void buildMaxHeap(int []a,int heapSize) {
for(int i = (heapSize-2)/2;i >= 0;i--) {
maxHeapfy(a,i,heapSize);
}
}
static void heapSort(int []a) {
int len = a.length;
buildMaxHeap(a,len); //初始建堆
a[len-1] = a[0] ^ a[len-1]; //交換
a[0] = a[0] ^ a[a.length-1];
a[len-1] = a[0] ^ a[len-1];
for(int i = 1;i<len-1;i++) { //初始建堆之後還要排a.length-2次
maxHeapfy(a,0,len-i);
a[len-1-i] = a[0] ^ a[len-1-i]; //交換
a[0] = a[0] ^ a[len-1-i];
a[len-1-i] = a[0] ^ a[len-1-i];
}
}
}
//輸出結果:
//[4, 5, 7, 16, 25, 27, 30, 34, 41, 55]