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