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