演算法與資料結構系列 ( 七 ) - 歸併排序- Merge Sort

Tim-AutumnWind發表於2020-05-30

前言

本章我們看看 O(n log n) 級別的排序演算法
很多人就會說了,我可以用這個系列前面的文章就可以優化到 O(n)級別
而且我在電腦執行的時候,基本都是 1ms 或者是0ms,很快咻咻咻的
我就不用看你這個撲街博主的文章了吧


No 我給大家看看在不同資料量下的情況

資料量 n * 2 n log n 速度比較
10 100 33 3倍
100 10000 664 15倍
1000 10^6 9966 100倍
10000 10^8 132877 753倍
100000 10^10 1660964 6020倍

大家可以看到,在較大資料量(10萬)的時候,O(n*2)O(n log n)效率差距可以達到 6000倍
最小也是近 3倍 的差距
大家可以想象一下,就算是 1ms 也是將近 6m的等待時間
然後大家又會說了,php本身自帶的函式,很快呀
我為什麼不用自帶的,花時間看你這個系列幹什麼?
撲街博主:”告辭,抱歉,打擾了!”
開個玩笑,大家可以看到哈!
在資料量越來越大的時候,也就是我們現在要看的這個 O(n log n) 優勢也會越來越大
所以我們通常都會說 O(n log n)O(n*2) 要快,並且隨著資料量增大,速度也會越來越明顯,也正是因為如此。我們就需要去理解演算法的思想,為什麼他會變快那麼多?怎麼做到的?在我工作中是否可以用到?
下面我們來試著理解一下 歸併排序法

歸併排序,先簡單瞭解一下思路

  • 首先我們有這麼一段資料,我們需要將他們重新整合有序
  • 第一次二分 level - 0
    :seven::two::one::five::four::six::nine::eight:
  • 在歸併排序當中,他是將資料進行二分
  • 第二次二分 level - 1
    :seven:   :two::one::five: | :four::six::nine::eight:
  • 再次二分 level - 2
    :seven:   :two: | :one::five: | :four::six: | :nine::eight:
  • 直到分成最小粒度 level - 3
    :seven: | :two: | :one: | :five: | :four: | :six: | :nine: | :eight:
  • 最小粒度 下,它本身就是有序的,然後進行歸併,此時的資料為
    :two::seven: | :one::five: | :four::six: |:eight::nine:
  • 然後再次向上歸併
    :one::two::five::seven: | :four::six::eight::nine:
  • 繼續向上歸併直至有序
    :one::two::four::five::six::seven::eight::nine:
  • 此時資料已然有序

歸併排序,小總結

  • 歸併排序 其實就是逐層二分後,再逐層進行歸併
  • 按照層log n級別,和每次我們合併的過程使用 O(n) 的複雜度處理
  • 那麼我們就設計出了 n log n 複雜度的演算法
  • 同時 歸併排序法 和之前的排序不一樣,我們不能夠在陣列內直接操作交換位置,而是需要建立一個新的臨時變數去儲存排序的資訊
  • 這也是 歸併排序法 的缺點,需要額外申請空間去儲存資料資訊
  • 但是按照目前我們計算機的儲存價格,時間的效率要比空間效率更珍貴
  • 撲街博主說了那麼多,可能大家還是無法理解如何將 歸併排序 寫成程式碼。畢竟狗博主也沒有說清楚
  • 歸併排序 涉及到了新陣列,所以我們需要有一個歸併過程

如何書寫歸併排序過程呢?

  • 所以我們需要定義好我們的演算法規律,比如以下這個陣列,如何合併成新陣列
    :one::two::five::seven: | :four::six::eight::nine:
第一次考察
  • 首先我們考察合併的陣列為下面箭頭的兩個
  • 我們分別定義為座標 i代表左側陣列座標
  • 座標 j 代表右側陣列座標
  • m 則代表新陣列座標
  • c 則代表中間分隔的位置 不然會出現邊界情況
  • l 則是代表左邊邊界
  • r 則是代表右邊邊界
  • 此時我們考察中的數字 1 < 4,則將 :one: 歸併到新陣列, 同時m + 1i+1,在繼續考察
    m
    :one:
    i                         j
    :arrow_down:                           :arrow_down:
    :one::two::five::seven: | :four::six::eight::nine:
第二次考察
  • 接下來繼續考察 i :two:j :four:
           m
    :one::two:
            i                 j
            :arrow_down:                   :arrow_down:
    :one::two::five::seven: | :four::six::eight::nine:
  • 此時我們考察中的數字 :two:< :four:, 此時再將 :two: 歸併到新陣列,同時 m + 1i+1,再繼續考察
第三次考察
  • 接下來繼續考察 i :five:j :four:
           m
    :one::two::four:
                   i          j
                    :arrow_down:           :arrow_down:
    :one::two::five::seven: | :four::six::eight::nine:
  • 此時我們考察中的數字 :five:> :four:, 此時再將 :four: 歸併到新陣列,同時 m + 1i+1,再繼續考察
第四次考察
  • 接下來繼續考察 i :five:j :six:
                   m
    :one::two::four:
                   i                  j
                    :arrow_down:                   :arrow_down:
    :one::two::five::seven: | :four::six::eight::nine:
  • 此時我們考察中的數字 :five:< :six:, 此時再將 :five: 歸併到新陣列,同時 m + 1i+1,再繼續考察

    然後再持續的以此類推

  • 我們就會得到我們想要的排序
  • ps: 上面陣列沒有 :three:

TimAutumnWind (轉載請註明出處 learnku.com/users/48310


實現一下程式碼

/**
 * 歸併排序
 * @param array $sort
 * @param int $n
 * @return array
 */
function get_merge(array $sort,int $n):array
{
    get_merge_sort($sort,0,$n - 1);
    return $sort;
}

/**
 * 遞迴使用歸併排序,對 sort[l...r]的資料進行排序
 * @param array $sort
 * @param int $l
 * @param int $r
 * @return array
 */
function get_merge_sort(array &$sort,int $l,int $r)
{
    /**
     * 當 $l 小於 $r 的時候,當前處理的部分最低有兩個元素,需要進行排序
     * 但是當 $l 大於 $r 的時候,我們只需要處理一個元素,或者一個元素都沒有
     */
    if( $l >= $r){
        /** 此處必須終止 */
        return [];
    }

    /**
     * 當 $l + $r 非常大的時候有可能會出現邊界問題
     * 之前二分查詢就出現過這個bug
     * 本次主要是 歸併排序 所以暫不做處理,之後會有 二分查詢法 的文章
     */
    $mid = floor(($l + $r) / 2);

    /** 處理左邊 */
    get_merge_sort($sort,$l,$mid);

    /** 處理右邊 */
    get_merge_sort($sort,$mid +  1,$r);

    /** 處理合並 */
    get_merge_sort_help($sort,$l,$mid,$r);

}

/**
 * 將 sort[l...mid] 和 sort[mid+1...r] 兩部分進行合併
 * @param array $sort
 * @param int $l
 * @param int $mid
 * @param int $r
 */
function get_merge_sort_help(array &$sort,int $l,int $mid,int $r)
{

    $merge  = array();
    for ($i = $l; $i <= $r;$i++){
        $merge[$i-$l] = $sort[$i];
    }
    $i = $l;
    $j = $mid + 1;
    for ($m = $l; $m <= $r; $m++){
        if($i > $mid){
            $sort[$m] = $merge[$j-$l];
            $j++;
        }else if($j > $r){
            $sort[$m] = $merge[$i-$l];
            $i++;
        }else if($merge[$i-$l] < $merge[$j-$l]){
            $sort[$m] = $merge[$i-$l];
            $i++;
        }else{
            $sort[$m] = $merge[$j-$l];
            $j++;
        }
    }
}

大概效能測算

  • 本演算法基於 php 中的 for迴圈,可能效能要比 while 差一點
  • 物理機 Mac 記憶體:16GB / 處理器:2.3 GHz Intel Core i5
  • 執行環境為 docker
  • 本次測算效能因環境不同和機器情況不同,不具備實際意義,勿槓
  • 資料量:100 大約為 1ms - 5ms
  • 資料量:1000 大約為 45ms - 50ms
  • 資料量:10000 大約為 490ms - 495ms
  • 資料量:100000 大約為 5100ms - 5300ms
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章