堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
我們將給定的數組想象成一個完全二叉樹,那么數組元素與二叉樹節點的對應關系如下:
可以看到 0 的子元素為 1 、 2 , 1 的子元素為 3 , 4 、 3 的子元素為 7 、 8。
及對應關系為:下標為 n 的元素的左子元素下標為 2n+1 , 右子元素下標為 2n+2 。根據該對應關系,我們可以將數組看作一個滿足堆積性質的完全二叉樹,借助二叉樹的性質來進行排序。
簡單來說:堆排序是將數據看成是完全二叉樹、根據完全二叉樹的特性來進行排序的一種算法。
按堆積性質,堆可以分為 最大堆 和 最小堆:最大堆要求節點的元素都要不小於其孩子,最小堆要求節點元素都不大於其左右孩子。那么以最大堆為例,處於最大堆的根節點的元素一定是這個堆中的最大值。
下面僅討論最大堆:
我們從最底層節點開始構建最大堆,依次向上。因為每個節點的兩棵子樹已經被我們構建為了最大堆,所以選擇兩個子樹的根節點及當前節點中的最大值即為以當前節點為根的樹中的最大值。
及自下向上構建最大堆時,我們在每一層只需比較根元素及其兩個孩子節點即可正確的構建最大堆。我們對一個兩層的完全二叉樹的建堆代碼如下:
/** * @Author Nxy * @Date 2019/12/5 * @Param head:堆頂位置;end:序號為end及之前的元素可以用來構建堆 * @Return * @Exception * @Description 將一個只有三個節點的堆變為最大堆 */ public static void sortNode(int[] nums, int head, int end) { if (head < 0) { throw new RuntimeException("堆頂超過左邊界"); } int length = nums.length; //左子節點坐標 int left = head * 2 + 1; //右子節點坐標 int right = left + 1; //判斷左子節點是否存在 if (left <= end) { //如果左子節點更大,交換 if (nums[left] > nums[head]) { int temp = nums[head]; nums[head] = nums[left]; nums[left] = temp; } } //判斷右子節點是否存在 if (right <= end) { //如果右子節點更大,交換 if (nums[right] > nums[head]) { int temp = nums[head]; nums[head] = nums[right]; nums[right] = temp; } } }
基於對兩層完全二叉樹的建堆,我們可以將整個數組構建為一個最大堆:
/** * @Author Nxy * @Date 2019/12/5 * @Param * @Return * @Exception * @Description 排序一個整堆, 最底層最右邊的葉子節點序號為end。最小的非葉子節點序號為 (end - 1) / 2 * 排序是從最小的非葉子節點構建堆,向上直到根節點 */ public static void sortHeap(int[] nums, int end) { int length = nums.length; for (int i = (end - 1) / 2; i >= 0; i--) { sortNode(nums, i, end); } }
但是需要注意的是,最大堆使得以二叉樹的角度理解數組,數組是符合堆積性質的,但數組並不是有序的。
以下面的最大堆為例:
可以看出,這是一個符合堆積性質的最大堆,但它並不是一個有序的數組,我們將其還原成數組:
這是因為,堆積性質只規定了根元素與孩子節點之間的大小關系,至於左孩子與右孩子之間的大小關系是沒有限制的。也就是說,根節點一定大於兩個孩子節點,但兩個孩子節點間是無序的。
所以構建最大堆對於排序來說,意義是為我們找到了序列中的最大值,而不是讓整個數組變的有序。
我們可以通過不斷的通過建堆找出剩余元素中的極值來完成排序。我們完整的排序操作如下:
1. 將整個數組構建為一個最大堆,此時 nums[0] 即是數組中的最大值。
2. 將 num[0] 與 nums[length-1] 互換。
3. 將 0到length-2 將的元素構建為最大堆,此時 nums[0] 即是數組中除第一步找出的最大值外的的最大值。
4. 將 num[0] 與 nums[length-2] 互換。
5. 重復上述操作,直到可以用來構建堆的元素僅剩 nums[0], 整個數組排序完畢。
可以看到,我們對 n 個元素建堆一次,需要 log2n 次計算,而整個過程需要建堆 n 次,所以時間復雜度為 O(nlog2n ) 。而我們在排序過程中采用了互換位置的就地排序方式,沒有使用額外的空間存儲數組,空間復雜度為 O(1) 。因為我們進行了多次建堆,數值相同的元素被冒到堆頂的順序是不確定的,因此堆排序是不穩定排序。
基於上面展示的兩個方法,堆排序的實現為:
/** * @Author Nxy * @Date 2019/12/6 * @Param * @Return * @Exception * @Description 堆排序 */ public static void heapSort(int[] nums) { if (nums == null) { throw new RuntimeException("數組為空"); } int length = nums.length; for (int i = length - 1; i >= 0; i--) { sortHeap(nums, i); int temp = nums[0]; nums[0] = nums[i]; nums[i] = temp; } }