在之前我寫過關於歸併排序的介紹,《排序演算法學習之路——歸併排序》。據現在已經有很長時間了。現在再重新進行規整,對歸併排序再從程式碼層面詳細說一下。
歸併排序演算法
按照慣例,對於排序演算法。我們還是先羅列概念
歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(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]); } }
我們再看程式碼明顯多出許多來。但是這個過程並不複雜,理解了非遞迴的方式更有助於我們對歸併排序的理解。