本博客總結學習堆排序算法,以一個數組爲例,採用大根堆進行升序排序,附有代碼實現。
堆排序的思想
堆排序的邏輯是建立在完全二叉樹的基礎上。
有兩個概念必須要瞭解:
- 大根堆:每個結點值都大於等於左右孩子結點值
- 小根堆:每個結點值都小於等於左右孩子結點值
以大根堆爲例,將根結點與最後一個結點交換,彈出根結點,即可得到整個樹中的最大值。繼續,將剩下的n-1個結點的樹再調整爲大根堆,再彈出根結點,以此類推,可得到一個有序序列。
問題的關鍵在於,如何進行堆調整?
我們把二叉樹中每一簇“父結點、左孩子、右孩子”當成一個三元組,從二叉樹底層開始,由下往上,依次對每一個三元組進行調整,套一兩層循環,即可完成堆調整。這是直觀的總體思路。
存在一個問題:如何根據父或子結點快速獲取三元組?
說白了就是需要建立父結點和孩子結點之間的聯繫。可通過完全二叉樹的性質來解決。完全二叉樹中,若按照層序遍歷對每個結點進行編號(從1開始),父節點爲 k ,則左右孩子結點編號一定爲 2 * k 和 2 * k + 1 。根據此性質可在父子結點之間快速互相訪問。
把待排序的數組看做完全二叉樹層序遍歷的結果,即可應用這個性質。如下圖所示:
代碼示例
先上代碼:
private void heapSort(int[] arr) {
int len = arr.length;
//將亂序數組調整爲大根堆
for (int i = len / 2 - 1; i > -1; --i) {
heapAdjust(arr, i, len);
}
//元素出堆、循環堆調整
for (int i = len - 1; i > 0; --i) {
//交換i和0兩個元素,使用位運算完成
arr[i] ^= arr[0];
arr[0] ^= arr[i];
arr[i] ^= arr[0];
heapAdjust(arr, 0, i);
}
//arr排序完畢
}
/**
* 堆調整
*/
private void heapAdjust(int[] arr, int s, int length) {
int temp = arr[s];
for (int j = 2 * s + 1; j < length; j *= 2) {
if (j + 1 < length && arr[j + 1] > arr[j]) {
++j;
}
if (temp > arr[j]) break;
arr[s] = arr[j];
s = j;
}
arr[s] = temp;
}
堆排序流程
1.將亂序數組調整爲大根堆
對於一個雜亂無章的數組而言,一層循環不足以將其調整爲大根堆,需要兩層。
- 外層循環:相當於從下往上遍歷所有的三元組;
- 內層循環:用子函數heapAdjust實現。按照直觀思路,此處不應該有循環,直接調整三元組即可(將父結點與某個孩子結點交換)。但是,每次調整後,孩子結點的值發生改變,該孩子結點值可能比下層結點小。因此需要循環對每一個發生改變的孩子結點的下層三元組進行修正。
2.元素出堆、循環堆調整
交換根節點與最後一個結點,把最大值移到了數組的末尾。再對前 n-1 個數進行堆調整,再次將最大值移到末尾,依次循環,即可得到升序排序結果。
注意:此處的堆調整不需要第一步中的兩層循環,只需要一層,調用heapAdjust即可。因爲前 n-1 個數中,只有arr[0]這一個位置不正確,並不是完全亂序,只需要調整這一個位置即可。
堆調整
堆調整是本算法中最核心的部分。即調整以 s 爲根的三元組爲正確的大根堆/小根堆,並對下層結點進行循環修正。
注意:此方法並不會遍歷整顆二叉樹,也不能將一棵雜亂的二叉樹調整爲大/小根堆
本部分代碼很巧妙,需要細細品讀。每次調整時,並不是直接交換父結點值和子結點值,那樣會徒增賦值次數。