【數據結構與算法】堆排序總結與實現

本博客總結學習堆排序算法,以一個數組爲例,採用大根堆進行升序排序,附有代碼實現。算法

堆排序的思想

堆排序的邏輯是創建在徹底二叉樹的基礎上。數組

有兩個概念必需要了解:函數

  • 大根堆:每一個結點值都大於等於左右孩子結點值
  • 小根堆:每一個結點值都小於等於左右孩子結點值

以大根堆爲例,將根結點與最後一個結點交換,彈出根結點,便可獲得整個樹中的最大值。繼續,將剩下的n-1個結點的樹再調整爲大根堆,再彈出根結點,以此類推,可獲得一個有序序列。性能

問題的關鍵在於,如何進行堆調整?學習

咱們把二叉樹中每一簇「父結點、左孩子、右孩子」當成一個三元組,從二叉樹底層開始,由下往上,依次對每個三元組進行調整,套一兩層循環,便可完成堆調整。這是直觀的整體思路。優化

存在一個問題:如何根據父或子結點快速獲取三元組?code

說白了就是須要創建父結點和孩子結點之間的聯繫。可經過徹底二叉樹的性質來解決。徹底二叉樹中,若按照層序遍歷對每一個結點進行編號(從1開始),父節點爲 k ,則左右孩子結點編號必定爲 2 * k 和 2 * k + 1 。根據此性質可在父子結點之間快速互相訪問。排序

把待排序的數組看作徹底二叉樹層序遍歷的結果,便可應用這個性質。以下圖所示:
image博客

代碼示例

先上代碼:基礎

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;
    }

堆排序流程

1.將亂序數組調整爲大根堆

對於一個雜亂無章的數組而言,一層循環不足以將其調整爲大根堆,須要兩層。

  • 外層循環:至關於從下往上遍歷全部的三元組;
  • 內層循環:用子函數heapAdjust實現。按照直觀思路,此處不該該有循環,直接調整三元組便可(將父結點與某個孩子結點交換)。可是,每次調整後,孩子結點的值發生改變,該孩子結點值可能比下層結點小。所以須要循環對每個發生改變的孩子結點的下層三元組進行修正。

2.元素出堆、循環堆調整

交換根節點與最後一個結點,把最大值移到了數組的末尾。再對前 n-1 個數進行堆調整,再次將最大值移到末尾,依次循環,便可獲得升序排序結果。

注意:此處的堆調整不須要第一步中的兩層循環,只須要一層,調用heapAdjust便可。由於前 n-1 個數中,只有arr[0]這一個位置不正確,並非徹底亂序,只須要調整這一個位置便可。

堆調整

堆調整是本算法中最核心的部分。即調整以 s 爲根的三元組爲正確的大根堆/小根堆,並對下層結點進行循環修正。

注意:此方法並不會遍歷整顆二叉樹,也不能將一棵雜亂的二叉樹調整爲大/小根堆

本部分代碼很巧妙,須要細細品讀。每次調整時,並非直接交換父結點值和子結點值,那樣會徒增賦值次數。

堆排序特色

  • 時間複雜度:最壞O(Nlog2N),平均性能接近最壞性能;空間複雜度O(1);
  • 不穩定排序;
  • 只能用於順序結構,不能用於鏈式結構;
  • 初始建堆比較次數較多,記錄少時不宜採用,記錄較多時比較高效。
相關文章
相關標籤/搜索