【演算法與資料結構專場】堆排序是什麼鬼?

帥地發表於2018-09-29

排序演算法相必大家都見過很多種,例如快速排序、歸併排序、氣泡排序等等。今天,我們就來簡單講講堆排序

在上一篇中,我們講解了二叉堆,今天的堆排序演算法主要就是依賴於二叉堆來完成的,不清楚二叉堆是什麼鬼的,可以看下:

【演算法與資料結構】二叉堆是什麼鬼?

用輔助陣列來實現堆排序演算法

假如給你一個二叉堆,根據二叉堆的特性,你會怎麼使用二叉堆來實現堆排序呢?

我們都知道,二叉堆有一個很特殊的節點 --- 堆頂,堆頂要嘛是所有節點的最大元素,要嘛是最小元素,這主要取決於這個二叉堆是最小堆還是最大堆

今天,我們暫且選擇以最小堆來作為例子。

基於堆頂這個特點,我們就可以來實現我們的堆排序了。

大家看下面一個例子:

對於一個如圖有10個節點元素的二叉堆:

【演算法與資料結構專場】堆排序是什麼鬼?

我們把堆頂這個節點刪除,然後把刪除的節點放在一個輔助陣列help裡。

【演算法與資料結構專場】堆排序是什麼鬼?

顯然,這個被刪除的節點,是堆中最小的節點。接下來,我們繼續刪除二叉堆的堆頂,然後把刪除的元素還是存放在help陣列裡。

【演算法與資料結構專場】堆排序是什麼鬼?

顯然,第二次刪除的節點,是原始二叉堆中的第二小節點。

繼續刪除

【演算法與資料結構專場】堆排序是什麼鬼?

繼續連續6次刪除堆頂,把刪除的節點一次放入help陣列。

【演算法與資料結構專場】堆排序是什麼鬼?

二叉堆中只剩最後一個節點了,這個節點同時也是原始二叉堆中的最大節點,把這個節點繼續刪除了,還是放入help陣列裡。

【演算法與資料結構專場】堆排序是什麼鬼?

此時,二叉堆的元素被刪除光了,觀察一下help陣列。這是一個有序的陣列,實際上,通過從二叉堆的堆頂逐個取出最小值,存放在另一個輔助的陣列裡,當二叉堆被取光之時,我們就完成了一次堆排序了。

其實無需輔助陣列

在上面的堆排序過程中,我們使用了一個輔助陣列help。可事實上,我們真的需要輔助陣列嗎?

上篇講二叉堆的時候,我們說過。二叉堆在實現的時候,是採取陣列的形式來儲存的。

從二叉堆中刪除一個元素,為了充分利用空間,其實我們是可以把刪除的元素直接存放在二叉堆的最後一個元素那裡的。例如:

【演算法與資料結構專場】堆排序是什麼鬼?

刪除堆頂,把刪除的元素放在最後一個元素。

【演算法與資料結構專場】堆排序是什麼鬼?

繼續刪除,把刪除的元素放在最後第二個位置

【演算法與資料結構專場】堆排序是什麼鬼?

繼續刪除,把刪除的元素放在最後第三個位置

【演算法與資料結構專場】堆排序是什麼鬼?

以此類推....

【演算法與資料結構專場】堆排序是什麼鬼?

這樣,對於一個含有n個元素的二叉堆,經過n-1(不用刪除n次)次刪除之後,這個陣列就是一個有序陣列了。

【演算法與資料結構專場】堆排序是什麼鬼?

所以,給你一個無序的陣列,我們需要把這個陣列構建成二叉堆,然後在通過堆頂逐個刪除的方式來實現堆排序。

其實,也不算是刪除了,相當於是把堆頂的元素與堆尾部在交換位置,然後在通過下沉的方式,把二叉樹恢復成二叉堆。

程式碼如下:

public class HeapSort {
    /**
     *  下沉操作,執行刪除操作相當於把最後
     *  * 一個元素賦給根元素之後,然後對根元素執行下沉操作
     * @param arr
     * @param parent 要下沉元素的下標
     * @param length 陣列長度
     */
    public static int[] downAdjust(int[] arr, int parent, int length) {
        //臨時保證要下沉的元素
        int temp = arr[parent];
        //定位左孩子節點位置
        int child = 2 * parent + 1;
        //開始下沉
        while (child < length) {
            //如果右孩子節點比左孩子小,則定位到右孩子
            if (child + 1 < length && arr[child] > arr[child + 1]) {
                child++;
            }
            //如果父節點比孩子節點小或等於,則下沉結束
            if (temp <= arr[child])
                break;
            //單向賦值
            arr[parent] = arr[child];
            parent = child;
            child = 2 * parent + 1;
        }
        arr[parent] = temp;
        return arr;
    }

    //堆排序
    public static int[] heapSort(int[] arr, int length) {
        //構建二叉堆
        for (int i = (length - 2) / 2; i >= 0; i--) {
            arr = downAdjust(arr, i, length);
        }
        //進行堆排序
        for (int i = length - 1; i >= 1; i--) {
            //把堆頂的元素與最後一個元素交換
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            //下沉調整
            arr = downAdjust(arr, 0, i);
        }
        return arr;
    }
    //測試
    public static void main(String[] args) {
        int[] arr = new int[]{1, 3, 5,2, 0,10,6};
        System.out.println(Arrays.toString(arr));
        arr = heapSort(arr, arr.length);
        System.out.println(Arrays.toString(arr));
    }
}

複製程式碼

對於堆的時間複雜度,我就直接給出了,有興趣的可以自己推理下,還是不難的。堆的時間複雜度是 O (nlogn)。空間複雜度是 O(1)。

這裡可能大家會問,堆排序的時間複雜度是O (nlogn),像快速排序,歸併排序的時間複雜度也是 O(nlogn),那我在使用的時候該如何選擇呢?

這裡說明一下:快速排序是平均複雜度 O(logn),實際上,快速排序的最壞時間複雜度是O(n^2。),而像歸併排序,堆排序,都穩定在O(nlogn)

我給出一個問題,例如給你一個擁有n個元素的無序陣列,要你找出第 k 個大的數,那麼你會選擇哪種排序呢?

顯然在這個問題中,選用堆排序是最好的,我們不用把陣列全部排序,只需要排序到前k個數就可以了。至於程式碼如何實現,這個我就不給程式碼了,大家可以動手敲一敲。

推薦閱讀:

【演算法與資料結構】二叉堆是什麼鬼?

獲取更多原創文章,可以關注下我的公眾號:苦逼的碼農,我會不定期分享一些資源和軟體等。

相關文章