高階資料結構---堆樹和堆排序

白露非霜發表於2020-05-02

堆樹介紹:

之前在二叉樹的時候說到過一種特殊的二叉樹---完全二叉樹(除了最後一層,其他層的每個結點都是滿的,且最後一層結點全部靠左排列,這樣就可以很方便的用陣列來表示,下標從0開始如果父結點索引是i那麼它兩個子結點的索引就是2i+12i+2,具體的圖解見二叉樹)。而堆樹又是一種特殊的完全二叉樹。它的每一個結點值都大於等於或者小於等於其左右結點的值。這裡和二叉搜尋樹不一樣,搜尋樹是左節點小於根,右結點大於根。為什麼是大於等於或者小於等於呢,如果大於等於,那麼根就是最大的數,這樣的就是大頂堆;如果是小於等於,那麼根就是最小的數,這樣就是小頂堆。

 

堆樹的操作:

插入:

堆樹插入之後要進行一個堆化的操作,也就是讓這棵樹滿足堆樹的性質。

一種是從下往上另一種是從上往下的堆化。

從下往上:

因為完全二叉樹用資料構造之後,那麼新插入的資料都在最後,但是插入之後,可能就不滿足堆樹的要求了,所以需要進行變動。以上圖的大頂堆為例,我們在最後插入9之後是不滿足堆樹的性質的。所以我們需要與其父結點進行交換,直到不能再交換位置。不用擔心資料量大了交換的次數問題,完全二叉樹近似一個滿二叉樹,最多交換logn-1次,想想232的次方是多少,這麼大的資料最多才31次。

 

刪除:

假設我們要刪除掉根結點10,並且刪除之後,還要滿足堆樹的性質。

如何才能刪除之後還能保證性質呢。其實就是將要刪除的元素和最後一個元素交換之後,刪除最後一個元素之後,再從上往下進行堆化的操作。

 

修改資料之後,同樣要進行堆化操作,根據修改之後的資料和他父結點和子結點比較來決定是向上還是向下進行堆化操作。

 

堆排序:

排序演算法一種,就是先將陣列構造成堆樹,再進行排序。那麼怎麼將一個陣列構造成堆樹。其實一個陣列我們都可以看成是一棵完全二叉樹,但是需要將這棵樹進行改造,才能得到堆樹。如何改造?

比如這個陣列:[8 4 20 7 3 1 25 14 17]

樹結構:

 

 我們從最後一個非葉子結點逆向依次進行向下堆化操作,如上圖也就是先從7(最後一個非葉子結點)開始向下堆化,7會和17進行對換,然後逆向一次執行,接下來就是20向下執行堆化,會和25對換;然後是4,這個時候會將剛才換上來17換上去,然後4繼續和下面比較,和14進行交換.......依次這樣操作之後就可以得到一棵堆樹了。圖解如下:

 

 

那麼得到堆樹之後,看起來也是無序的啊,怎麼就能實現排序呢?

可以看到堆樹的根要麼是最大的數要麼是最小的數,那我們依次將根取出來,之後再進行堆化操作,又會得到剩餘數中最大或者最小的。所以,我們將根與最後一個數交換之後,再進行堆化操作(這裡的堆化操作就要除開最後一個點了),然後再新的根和倒數第二個交換再堆化,依次這樣操作,後面數慢慢的都是有序的了。

圖解如下:

 

程式碼實現陣列轉堆樹和堆排序(不穩定):

 

    /**
     * 堆排序
     * @param arr
     */
    private static void heapSort(int[] arr) {
        int len = arr.length;
        /**
         * 陣列構造堆樹,從倒數第一個非葉子結點開始逆向一次進行堆化操作。最後一個葉子結點的父結點就是最後一個非葉子結點。
         * 索引從0開始。兩個子結點索引是2i+1和2i+2,所以最後一個非葉子結點的索引就是 len/2 - 1
         */
        for (int i = len / 2 - 1; i >=0 ; i--) { //時間複雜度nlogn
            createMaxHeap(arr, i, len);
        }
        for (int i = len - 1; i > 0 ; i--) { //時間複雜度nlogn
            int maxData = arr[0]; //第一個數最大
            arr[0] = arr[i];
            arr[i] = maxData;
            /**
             * 交換第一個最後一個位置,然後重新構造大頂堆。每迴圈一次就構造好了一個數的位置,
             * 最後到i為止都是排好序的,堆化的時候不需要再操作了
             */
            createMaxHeap(arr, 0, i);
        }
    }
    /**
     * 大頂堆構造及堆化過程
     * @param arr
     * @param start  
     * @param end  end之後是已經排好序的,所以需要end下標來判斷截止
     */
    private static void createMaxHeap(int[] arr, int start, int end) {
        int parentIndex = start;
        int leftChildIndex = 2 * parentIndex + 1;
        while (leftChildIndex < end) {
            int tempIndex = leftChildIndex;
            //比較左右結點誰大,記錄誰的下標
            if (leftChildIndex + 1 < end && arr[leftChildIndex] < arr[leftChildIndex + 1]) {
                tempIndex = leftChildIndex + 1;
            }
            //父結點比孩子大,不交換
            if (arr[parentIndex] > arr[tempIndex]) {
                return;
            }
            else {
                //交換資料,重新整理父結點繼續執行堆化操作
                int tempData = arr[parentIndex];
                arr[parentIndex] = arr[tempIndex];
                arr[tempIndex] = tempData;
                parentIndex = tempIndex;
                leftChildIndex = 2 * parentIndex + 1;
            }
        }
    }

 堆樹和堆排序在JDK中的應用,可以參見優先佇列:java.util.PriorityQueue

相關文章