在處理高併發有一個理論就是分治思想,非常巧妙。我們可以借鑑這個思想,來解決非排序的問題。說排序之前先講講什麼是遞歸方法。這對後面的理解有很大的幫助。
遞歸
歸併排序和快速排序,都借用了遞歸算法,分治是一種解決問題的處理思想,遞歸是一種編程技巧。是一個非常標準的遞歸求解問題的分解過程,去的過程叫“遞”,回來的過程叫“歸”。
遞歸需要滿足的三個條件:
- 一個問題的解可以分解爲幾個子問題的解
- 這個問題與分解之後的子問題,除了數據規模不同,求解思路完全一樣
- 存在遞歸終止條件
歸併排序(Merge Sort)
如果要排序一個數組,我們先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,再將排好序的兩部分合並在一起,這樣整個數組就都有序了。下圖來自極客時間的專欄。
// 歸併排序主程序
function mergeSort($arr) {
$len = count($arr);
if ($len <= 1) {
return $arr;
} // 遞歸結束條件, 到達這步的時候, 數組就只剩下一個元素了, 也就是分離了數組
$mid = intval($len / 2); // 取數組中間
$left = array_slice($arr, 0, $mid); // 拆分數組0-mid這部分給左邊left
$right = array_slice($arr, $mid); // 拆分數組mid-末尾這部分給右邊right
$left = mergeSort($left); // 左邊拆分完後開始遞歸合併往上走
$right = mergeSort($right); // 右邊拆分完畢開始遞歸往上走
$arr = merge($left, $right); // 合併兩個數組,繼續遞歸
return $arr;
}
// merge函數將指定的兩個有序數組(arrA, arr)合併並且排序
function merge($arrA, $arrB) {
$arrC = array();
while (count($arrA) && count($arrB)) {
// 這裏不斷的判斷哪個值小, 就將小的值給到arrC, 但是到最後肯定要剩下幾個值,
// 不是剩下arrA裏面的就是剩下arrB裏面的而且這幾個有序的值, 肯定比arrC裏面所有的值都大所以使用
$arrC[] = $arrA[0] < $arrB[0] ? array_shift($arrA) : array_shift($arrB);
}
return array_merge($arrC, $arrA, $arrB);
}
上面是php代碼實現算法的實現方式,看完圖和代碼,是不是對歸併算法有了新的認識。
歸併排序是穩定的排序算法嗎?結合我前面畫的那張圖和歸併排序的僞代碼,你應該能發現,歸併排序穩不穩定關鍵要看 merge() 函數,也就是兩個有序子數組合併成一個有序數組的那部分代碼。歸併排序是一個穩定的排序算法。
歸併排序的時間複雜度是多少?從我們的原理分析和僞代碼可以看出,歸併排序的執行效率與要排序的原始數組的有序程度無關,所以其時間複雜度是非常穩定的,不管是最好情況、最壞情況,還是平均情況,時間複雜度都是 O(nlogn)。
快速排序(Quicksort)
快排使用的也是分治思想,區別于歸並排序是因爲它會設置一個pivot(分區點),我們遍歷 p 到 r 之間的數據,將小於 pivot 的放到左邊,將大於 pivot 的放到右邊,將 pivot 放到中間。經過這一步驟之後,數組 p 到 r 之間的數據就被分成了三個部分,前面 p 到 q-1 之間都是小於 pivot 的,中間是 pivot,後面的 q+1 到 r 之間是大於 pivot 的。有點像數據結構中的二叉樹,左小右大,有序排列。
歸併排序的處理過程是由下到上的,先處理子問題,然後再合併。而快排正好相反,它的處理過程是由上到下的,先分區,然後再處理子問題。歸併排序雖然是穩定的、時間複雜度爲 O(nlogn) 的排序算法,但是它是非原地排序算法。我們前面講過,歸併之所以是非原地排序算法,主要原因是合併函數無法在原地執行。快速排序通過設計巧妙的原地分區函數,可以實現原地排序,解決了歸併排序佔用太多內存的問題。
快排也是用遞歸來實現的。對於遞歸代碼的時間複雜度,我前面總結的公式,這裏也還是適用的。如果每次分區操作,都能正好把數組分成大小接近相等的兩個小區間,那快排的時間複雜度遞推求解公式跟歸併是相同的。所以,快排的時間複雜度也是 O(nlogn)。
/**
* @param $array
* @return array 要排序的數組
*/
function quick_sort($array)
{
// 判斷是否需要運行,因下面已拿出一箇中間值,這裏<=1
if (count($array) <= 1) {
return $array;
}
$middle = $array[0]; // 中間值
$left = array(); // 接收小於中間值
$right = array();// 接收大於中間值
// 循環比較
for ($i=1; $i < count($array); $i++) {
if ($middle < $array[$i]) {
// 大於中間值
$right[] = $array[$i];
} else {
// 小於中間值
$left[] = $array[$i];
}
}
// 遞歸排序劃分好的2邊
$left = quick_sort($left);
$right = quick_sort($right);
// 合併排序後的數據,別忘了合併中間值
return array_merge($left, array($middle), $right);
}