排序算法-堆排序(Heap Sort)

堆排序法介紹

堆排序是對簡單選擇排序法的改進算法,堆排序結合完全二叉樹的性質,將序列和完全二叉樹結合,每次比較都記錄了比較結果,始終維護了每輪比較的最大值或者最小值。
在這裏插入圖片描述
上面兩個完全二叉樹分別是大頂堆和小頂堆,我們發現大頂堆的每個父結點都比子結點要大,而小頂堆的父結點比每個子結點都要小。

對於堆的定義是:完全二叉樹的每個結點的值都要大於或等於其左右子結點的值,稱爲大頂堆;完全二叉樹的每個結點的值都要小於或等於其左右子結點的值,稱爲小頂堆。

完全二叉樹的性質是堆排序的實現基礎,完全二叉樹的性質:
對一棵有n個結點的完全二叉樹的結點按層編號
1.如果結點i=1,則結點i是根結點;如果i>1,則其父結點是⌊i/2⌋;
2. 如果2i>n,則結點i無左孩子,i爲葉子結點,否則2i是其左孩子結點;
3. 如果2i+1>n,則結點i無右孩子結點,否則2i+1是其右孩子。

堆排序的基本思想是:將待排序的序列構造成一個大(小)頂堆,堆頂的根就是序列的最大(小)值。將堆頂與末尾的結點交換位置,也就是將堆頂的值與序列的末尾元素交換,然後將堆頂到n-1的位置重新構造一個新堆(因爲堆頂已經不是最大(小)的了)。反覆執行,最終將序列排序完畢。

【構造堆】
下面舉個堆構造過程例子:
假定我們有序列:{40,90,10,50,30,70,60,80,20}
在這裏插入圖片描述
在這裏插入圖片描述
可以看到標了顏色的結點都是有子結點的,也就是說,我們只需要調整這幾個父結點就可以構造一個堆了。並且我們發現,⌊n/2⌋就是擁有子結點的父結點數。首先從倒數第二層最右的父結點開始,如圖一。比較50小於80,替換位置。
堆構建順序按照層序排序的倒序進行(從最底層,每層從右到左),所以圖二繼續從10結點比較其子結點,10<70,替換位置。
如此反覆執行,最終構建成堆。
在這裏插入圖片描述
最終堆構造完之後的序列如下:
在這裏插入圖片描述
【堆排序】
堆構造完畢之後,就是堆排序了,排序的過程就是堆頂與堆尾交換位置,並不斷重新構造新堆的過程:
在這裏插入圖片描述
將堆頂的根結點和末尾結點交換位置,如圖一。交換位置之後,將樹根到n-1位置重新構成一個新的堆,使其堆頂保持爲最大(小)值。接下來與此往復,最終將整個序列排序完畢。
在這裏插入圖片描述

算法

構造堆算法

    public static void main(String[] args) {
    	Integer[] arr = new Integer[]{40,90,10,50,30,70,60,80,20};
    	for (int i = arr.length/2; i > 0; i--) {
            heapAdjust(arr, i, arr.length);
        }
        System.out.println(Arrays.toString(arr));
    }
    
    /**
     * 堆調整
     *
     * @param arr       待排序序列
     * @param heapAdjustIdx 調整結點下標,注意此處是根據完全二叉樹下標來,不是數組的下標。
     * @param heapEndIdx    堆尾下標,注意此處是根據完全二叉樹下標來,不是數組的下標。 1 ≤ heapEndIdx
     */
    public static void heapAdjust(Integer[] arr, int heapAdjustIdx, int heapEndIdx) {
        //特別說明,由於數組下標從0開始,而完全二叉樹編號從1開始,所以使用二叉樹編號從數組中取值需-1。
        int temp = arr[heapAdjustIdx - 1];//暫存調整結點值
        for (int i = heapAdjustIdx * 2; i <= heapEndIdx; i = i * 2) {// 根據完全二叉樹性質,結點i的左子節點爲i*2
            //變量i爲調整結點的左右子結點下標
            if (i < heapEndIdx && arr[i - 1] < arr[i]) { //如果左子結點小於右子結點;i < endIdx防止數組越界
                i++;//左子結點+1變爲右子結點下標
            }
            if (temp >= arr[i - 1]) {//如果當前調整結點不小於它的子結點,則不需要交換數據直接跳出循環
                break;
            }
            arr[heapAdjustIdx - 1] = arr[i - 1];//交換數據
            heapAdjustIdx = i; //將調整結點下標更改爲的替換子結點下標,繼續循環調整
        }
        arr[heapAdjustIdx - 1] = temp; //將原本暫存的結點值插入到最終位置
    }

排序算法

    public static void main(String[] args) {
    	Integer[] arr = new Integer[]{40,90,10,50,30,70,60,80,20};
    	heapSort(arr);
    	System.out.println(Arrays.toString(arr));
    }
    
    /**
     * 堆排序
     *
     * @param arr 待排序列表
     */
    public static void heapSort(Integer[] arr) {
        for (int i = arr.length / 2; i > 0; i--) { //此處除2是只需要調整有子結點的父結點
        	//構造堆
            heapAdjust(arr, i, arr.length);
        }

        for (int i = arr.length; i > 1; i--) {
            //交換堆頂結點和堆末尾的值
            Integer temp = arr[i - 1];
            arr[i - 1] = arr[0];
            arr[0] = temp;
            //重新調整根至n-1位置的二叉樹使其維持堆的性質(堆頂最大)
            heapAdjust(arr, 1, i - 1);
        }
    }

時間複雜度

堆的構建時間複雜度:O(n2)O(n^{2})
排序時間複雜度:O(nlogn)O(nlogn)

穩定性

由於對序列跳躍式維護堆的結構性質,相同值順序無法得到保障,所以堆排序是不穩定的。

發佈了69 篇原創文章 · 獲贊 86 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章