計算與數據結構篇 - 排序(歸併/快排)

在處理高併發有一個理論就是分治思想,非常巧妙。我們可以借鑑這個思想,來解決非排序的問題。說排序之前先講講什麼是遞歸方法。這對後面的理解有很大的幫助。

遞歸

歸併排序和快速排序,都借用了遞歸算法,分治是一種解決問題的處理思想,遞歸是一種編程技巧。是一個非常標準的遞歸求解問題的分解過程,去的過程叫“遞”,回來的過程叫“歸”。

遞歸需要滿足的三個條件:

  • 一個問題的解可以分解爲幾個子問題的解
  • 這個問題與分解之後的子問題,除了數據規模不同,求解思路完全一樣
  • 存在遞歸終止條件

歸併排序(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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章