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

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

堆排序的思想

堆排序的邏輯是建立在完全二叉樹的基礎上。

有兩個概念必須要瞭解:

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

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

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

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

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

說白了就是需要建立父結點和孩子結點之間的聯繫。可通過完全二叉樹的性質來解決。完全二叉樹中,若按照層序遍歷對每個結點進行編號(從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兩個元素,使用位運算完成
            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 爲根的三元組爲正確的大根堆/小根堆,並對下層結點進行循環修正。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章