【圖解演算法】排序演算法——堆排序
簡介
關於堆排序(HeapSort),堆這種資料結構比這種排序演算法更為有價值。
堆排序(Heapsort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是選擇排序的一種。可以利用陣列的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在陣列的非降序排序中,需要使用的就是大根堆,因為根據大根堆的要求可知,最大的值一定在堆頂。(from 百度百科)
這裡需要補充介紹一下堆這種資料結構:
堆通常是一個可以被看做一棵樹的陣列物件。堆總是滿足下列性質:
- 堆中某個節點的值總是不大於或不小於其父節點的值;
- 堆總是一棵完全二叉樹。
我們可以根據性質一的描述將堆分為最大堆(子節點永遠小於父節點)、最小堆(子節點永遠大於父節點),上圖所示是一個最大堆。我們可以在計算機中使用一個數組來儲存一個堆。
[ -, 90, 36, 17, 25, 26, 7, 1, 2, 3, 10 ]
注意我們將陣列下標為 0 的位置棄用了,根據圖示我們不難得出一個節點的父節點和子節點的下標位置。
- 父節點的位置: i/2
- 左子節點的位置: i*2
- 右子節點的位置: i*2 + 1
維護堆
維護一個堆需要做兩件事情:進隊和出隊
進隊
進隊需要做的事情很簡單,先將元素丟進佇列尾,然後和父節點進行比較,比父節點大時則於其交換位置,比父節點小時則表示進隊完成。
出隊
出隊像上圖所示,也非常簡單,只需要將佇列頭位置的元素取出(下標為 1 的元素),然後將佇列尾的元素填充到佇列頭,接下來和子節點較大者對換位置,直至比子節點的元素都要大時結束交換位置。
排序
在弄清楚了堆的維護之後我們只需要將陣列中的元素入隊和出隊,即可完成整個排序過程。
Java 實現
MaxHeap.java
/**
* 最大二叉堆
* 概念: 父結點的鍵值總是大於或等於任何一個子節點的鍵值
*/
public class MaxHeap {
// 模擬最大堆的陣列,記錄從 1 開始
private int[] heapArr;
// 當前位置的計數,由此可得出:
// 父節點位置 count/2 ;
// 左邊的子節點 count * 2 ;
// 右邊的子節點 count * 2 + 1 。
private int count;
// 堆的大小
private int length;
/**
* @param length 申明 heapArr 的長度
*/
public MaxHeap(int length) {
this.heapArr = new int[length + 1];
this.count = 0;
this.length = length;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
/**
* 插入一個新的元素
* @param item
*/
public void insert(int item) {
if(count + 1 > length) throw new ArrayIndexOutOfBoundsException();
heapArr[count + 1] = item;
count ++;
shiftUp(count);
}
/**
* 取出最大(優先)的元素
* @return 存在即取出改元素,若不存在則返回 Integer 最小值
*/
public int extractMax() {
if(count < 1) throw new ArrayIndexOutOfBoundsException();
int ret = heapArr[1];
swap(heapArr, 1, count );
count --;
shiftDown(1);
// 由於上面的 count-- 了,即 count >= 0 表明下標為 1 的位置存在值
return count >= 0?ret:Integer.MIN_VALUE;
}
/**
* 將堆中最大的元素出隊,並調整最大二叉堆的位置,保持最大二叉堆的定義
*/
private void shiftDown(int n) {
while (n*2 <= count) { // 當 n 位置的元素存在子節點時
// 較出左節點和右節點的較大的元素,然後將較大元素與 n 位置元素比較,若 n 位置元素較小則與其交換位置
int j = n*2; // 較大子節點下標,初始化為左子節點
if(j + 1 <= count) { // 存在右節點
j = heapArr[j + 1] > heapArr[j]?j+1:j;
}
if(heapArr[n] < heapArr[j]) {
swap(heapArr, n, j);
n = j;
}else {
break;
}
}
}
/**
* 調整位置為 n 的元素的位置,即保持最大二叉堆的定義 -> 父結點的鍵值總是大於或等於任何一個子節點的鍵值
* @param n 需要調整的元素的位置
*/
private void shiftUp(int n) {
while (n > 1 && heapArr[n/2] < heapArr[n]) {
swap(heapArr, n/2, n);
n = n/2;
}
}
/**
* 交換陣列中的兩個元素
* @param arr
* @param a
* @param b
*/
private void swap(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
測試方法
public static void main(String[] args) {
MaxHeap maxHeap = new MaxHeap(100);
int [] testArr = {2,7,26,25,19,17,1,90,3,36};
for (int i = 0; i < testArr.length; i++) {
maxHeap.insert(testArr[i]);
}
for (int i = 0; i < testArr.length; i++){
System.out.print( maxHeap.extractMax() );
if(i != testArr.length - 1) System.out.print(", ");
}
}
堆排序的過程
最後
過兩天就新年了,祝願各位朋友新年快樂!
還有需要一提的是: 該演算法存在優化的途徑,比如說在入隊時可以採取直接整個陣列入隊的方式;以及在交換方式上優化,使用賦值的方式取代(直接算出需要交換的值,避免多次交換值),諸君加油。