全面瞭解歸併排序演算法及程式碼實現

跡憶客發表於2021-12-09

在之前我寫過關於歸併排序的介紹,《排序演算法學習之路——歸併排序》。據現在已經有很長時間了。現在再重新進行規整,對歸併排序再從程式碼層面詳細說一下。

歸併排序演算法

按照慣例,對於排序演算法。我們還是先羅列概念

歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。


合併

通過概念我們也能看出,既然是歸併排序,那核心的問題就是如何進行歸併了。這可以歸結為從小往大的一個合併問題。

給定我們一組資料

 

我們通過分治策略,將其拆分。直到不能拆為止,想要達到的效果如下

 

這就是我們最終用來進行歸併的拆分後的陣列。下面開始合併

 

我們可以看到,每兩個陣列進行合併。合併之後的新陣列是有序的。然後新陣列之間再兩兩合併,直到合併為一個最終的陣列。

下面我們以最後一步合併為例子,介紹一下兩個陣列之間合併的細節步驟。

 

第一步、 sl 和 sr位置上的元素進行比較,值小的一方的元素放入新陣列,然後對應的索引 sl/sr 向前進一位。這裡 sl位置上的2小,因此將2 放入新陣列,sl移到該組的下一個元素

 

第二步、再次將 sl 和 sr 位置上的元素進行比較,3 比 6 小,因此 3放入新陣列,sl再次移動到下一個元素

 

第三步、二者繼續比較,此時 sr上的 6 要比 sl上的7小。因此將6放入新的陣列,sr移動到該組的下一個元素

 

第四步、重複上面的比較過程,直到有一組先於另一組全部放入到新陣列中

 

最後,此時的 sl一組已經全部排序完成了,而對於 sr一組剩餘的元素可以直接放入新陣列。因為每一組之內的元素都是有序的。

 

此時我們看到整個歸併過程已經完成了。下面我們看一下合併過程的程式碼(以下程式碼用 PHP 編寫)

function Merge($arr,$l,$m,$r)
{
    $t = $arr;
    $lstart = $l;
    $rstart = $m+1;

    while($l < $r) {
        if($lstart > $m || $rstart > $r) break;
        if($arr[$lstart] > $arr[$rstart]) {
            $t[$l++] = $arr[$rstart++];
        }else{
            $t[$l++] = $arr[$lstart++];
        }
    }

    $start = $l;
    $end = $r;

    if($lstart <= $m) {
        $start = $lstart;
        $end = $m;
    }elseif($rstart <= $r) {
        $start = $rstart;
        $end = $r;
    }

    while($start <= $end) {
        $t[$l++] = $arr[$start++];
    }
    $arr = $t;

    return $arr;
}

 


拆分

上面我們看到了歸併排序的核心的過程,合併。但是隻是有合併的過程還是不完整的,因為給到我們的原始資料是一組完整的資料。因此在合併之前我們應該先對其進行拆分。

拆分的過程比較簡單,它不會涉及到排序的問題,只是拆就完了。

 

這裡拆分過程的程式碼可以分為兩種方式:遞迴實現和非遞迴實現

下面我們分別看一下兩種不同的拆分程式碼

遞迴

遞迴方式程式碼就非常簡單了,我們只需要設定遞迴終止條件,然後按照一個整體的輪廓寫程式碼就可以了

function MergeSort(&$arr,$l,$r)
{
    if($l >= $r) {
        return;
    }
    $m = floor(($l+$r) / 2);

    MergeSort($arr,$l,$m);
    MergeSort($arr,$m+1,$r);
    $arr = Merge($arr,$l,$m,$r);
}

 

我們可以看到,程式碼很簡單。按照深度優先方式,先拆左邊,然後再拆右邊。最後呼叫我們上面的Merge() 函式進行合併就行了。

非遞迴

非遞迴的方式程式碼顯得就有點複雜了,在上面使用遞迴的方式拆分的過程中,其實我們只是設定了終止遞迴的條件,其他的細節不用考慮的。但是非遞迴方式就必須需要考慮細節了。

因為拆分的過程是一個深度優先的過程,針對深度優先這裡就需要用到棧機制。

其實,遞迴底層的實現機制也是藉助的棧的機制實現的。

這裡我們看一下程式碼

function MergeSort(&$arr)
{
    $stack = []; //  初始化一個棧
    $toMergeStack = []; // 用於歸併的棧
    $l = 0;
    $r = count($arr) - 1;

    $tmp = [$l,$r,floor(($l+$r)/2)];

    array_push($stack,$tmp);
    array_push($toMergeStack,$tmp);

    while(!empty($stack)) {
        $s = array_pop($stack);
        if($s[0] < $s[2]) {
            array_push($stack,[$s[0],$s[2],floor(($s[0]+$s[2])/2)]);
            array_push($toMergeStack,[$s[0],$s[2],floor(($s[0]+$s[2])/2)]);
        }

        if($s[2]+1 < $s[1]) {
            array_push($stack,[$s[2]+1,$s[1],floor(($s[2]+1+$s[1])/2)]);
            array_push($toMergeStack,[$s[2]+1,$s[1],floor(($s[2]+1+$s[1])/2)]);
        }
    }

    // 開始合併
    while(!empty($toMergeStack)){
        $s = array_pop($toMergeStack);
        $arr = Merge($arr,$s[0],$s[2],$s[1]);
    }
}

我們再看程式碼明顯多出許多來。但是這個過程並不複雜,理解了非遞迴的方式更有助於我們對歸併排序的理解。

相關文章