常見演算法 PHP 實現 -- 堆排序

Newiep發表於2018-07-22

昨晚又重新看了看以前一直沒仔細研究的堆排序,記錄篇文章,把思路記錄一下。歡迎大家訪問我的部落格。(荒廢了好久的部落格又開張了,這次用的是 gitpage,靜態網站生成器用的是 hugo

廢話不多說,進入正文

什麼是堆

通俗點講,堆(英語:Heap)是具有以下性質的完全二叉樹,任意節點小於(或大於)它的左右子節點,最小值(或大值)在根元素位置。我們稱最小值在根節點(堆頂)的叫做小根(頂)堆,最大值在根節點(堆頂)的叫做大根(頂)堆。

總結性質如下:

  • 任意節點小於(或大於)它的所有後裔,最小元(或最大元)在堆的根上(堆序性)。
  • 堆總是一棵完全樹。即除了最底層,其他層的節點都被元素填滿,且最底層儘可能地從左到右填入。

二叉樹性質

除了堆的定義外,我們還需要了解一些樹的基本性質,下面程式碼會用到這些。

我們假設一個陣列的第一個元素(索引為 0)位於根節點,那麼可以得出一下定義:

  • 任意節點 i 的左節點索引是 : 2 * i + 1
  • 任意節點 i 的右節點索引是 : 2 * i + 2
  • 最後一個非葉子節點的索引是 : floor(length / 2) - 1

堆排序

有了上面這些基礎後,我們就可以開始介紹一下堆排序的整體思路了。

  1. 將待排序陣列 (R0,R2....Rn) 構建為一個大(小)根堆;

  2. 將堆頂元素 R[0] 與最後一個元素 R[n] 交換,此時得到一個新的無序區 (R0,R2,......Rn-1) 和新的有序區 (Rn),且滿足 R[0,2...n-1] <= R[n];

  3. 此時堆頂的元素 R[0] 有可能違反堆的性質,因此我們需要重新重複第一步的操作,將 (R0,R2,......Rn-1) 重新構建為大(小)根堆;

  4. 迴圈執行上面的過程,直到有序區的元素為 n,則排序完成。

對於堆排序,最重要的兩個操作就是構造初始堆調整堆,其實構造初始堆也是調整堆的過程,只不過構造初始堆是對所有的非葉節點都進行調整,調整堆則只需要對堆頂元素 R[0] 做向下調整。

程式碼實現

程式碼實現分為兩部分,第一部分我們介紹如果把無序的陣列調整為一個符合堆性質的陣列

  • 對於第一次構建堆,我們選擇從第一個非葉子節點開始調整,一直迴圈到根節點為止

  • 對於調整堆,我們只需要從根節點開始,做一次調整就夠了,因為除了根節點外,其他的節點是滿足根節點性質的

上面描述的有點抽象,最好可以畫一個樹,然後一個節點一個節點調整。最難理解的大概就是構建堆的過程了。

/**
 * 構建堆
 *
 * @param  [array] $arr  [待排序無序陣列]
 * @param  [int] $start [第一個需要調整的非葉子節點]
 * @param  [int] $len  [元素個數]
 *
 * @return void
 */
function heapAdjust(&$arr, $start, $len)
{

    for ($child = $start * 2 + 1; $child < $len; $child = $child * 2 + 1) {
        //左節點小於右節點
        if ($child != $len - 1 && $arr[$child] < $arr[$child + 1]) {
            $child++;  //此時子節點指向右子節點
        }

        //滿足大頂(根)堆
        if ($arr[$start] >= $arr[$child]) {
            break;
        }

        //和子節點進行交換
        list($arr[$start], $arr[$child]) = array($arr[$child], $arr[$start]);

        $start = $child;
    }
}

第二步,我們來看看具體排序實現

/**
 * [heapSort 堆排序實現]
 *
 * @param  [array] $arr [待排序的無序陣列]
 *
 * @return void
 */
function heapSort(&$arr) 
{
    /**
     * 將待排序陣列構建成一個大頂(根)堆
     * 構建說明:從最後(下)一個非葉子節點,比較當前節點和子節點,找到最小的點,進行交換,
     * 迴圈向堆頂遞進,最後就形成了一個大頂(根)堆
     */
    $len = count($arr);

    //第一次構建堆,我們從第一個非葉子節點開始調整一直迴圈到根節點為止,則構造完成
    for ($i = ($len >> 1) - 1; $i >= 0; $i--) {
        heapAdjust($arr, $i, $len);
    }

    for ($i = $len - 1; $i >= 0; $i--) {
        //交換根頂和根尾元素
        list($arr[0], $arr[$i]) = array($arr[$i], $arr[0]);

        //調整堆,我們只需要從根節點開始向下調整
        heapAdjust($arr, 0, $i);
    }
}

時間複雜度與空間複雜度

堆執行一次調整需要O(logn)的時間,在排序過程中需要遍歷所有元素執行堆調整,所以最終時間複雜度是O(nlogn)。空間複雜度是O(1)。

總結

對堆排序理解的難點,大部分在於構建堆的過程的理解上,為何從第一個非葉子節點開始和子節點進行交換,一直迴圈到根頂,這個構建過程還需要親手畫畫圖,理解的會比較深。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章