1. 程式人生 > >排序演算法中——歸併排序和快速排序

排序演算法中——歸併排序和快速排序

氣泡排序、插入排序、選擇排序這三種演算法的時間複雜度都為 O ( n 2 ) O(n^2)

,只適合小規模的資料。今天,我們來認識兩種時間複雜度為 O ( n l o g n )
O(nlogn)
的排序演算法——歸併排序(Merge Sort)和快速排序(Quick Sort),他們都用到了分治思想,非常巧妙。

1. 歸併排序(Merge Sort)?

1.1. 歸併排序演算法實現

  • 歸併排序的核心思想其實很簡單,如果要排序一個數組,我們先把陣列從中間分成前後兩部分,然後分別對前後兩部分進行排序,再將排好序的兩部分資料合併在一起就可以了。

歸併排序

  • 歸併排序使用的是分治思想,分治也即是分而治之,將一個大問題分解為小的子問題來解決。分治演算法一般都是用遞迴來實現的。分治是一種解決問題的處理思想,遞迴是一種程式設計技巧

  • 如果要對陣列區間 [p, r] 的資料進行排序,我們先將資料拆分為兩部分 [p, q] 和 [q+1, r],其中 q 為中間位置。對兩部分資料排好序後,我們再將兩個子數組合並在一起。當陣列的起始位置小於等於終止位置時,說明此時只有一個元素,遞迴也就結束了。

遞推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

終止條件:
p >= r 不用再繼續分解
  • 對兩個子陣列進行合併的過程如下所示,我們先建立一個臨時陣列,然後從兩個子陣列的起始位置開始比較,將較小的元素一個一個放入臨時陣列,直到其中一個子陣列比較完畢,再將剩下的另一個子陣列餘下的值全部放到臨時陣列後面。最後我們需要將臨時陣列中的資料拷貝到原陣列對應的位置。

數組合並

  • 程式碼實現
// O(n(logn))
void Merge_Sort(float data[], int left, int right, float sorted_data[])
{
    if(left < right)
    {
        int mid = (left + right) / 2;
        Merge_Sort(data, left, mid, sorted_data);
        Merge_Sort(data, mid+1, right, sorted_data);
        Merge_Array(data, left, mid, right, sorted_data);
    }
}

void Merge_Array(float data[], int left, int mid, int right, float temp[])
{
    int i = left, j = mid + 1;
    int k = 0;

    // 從子陣列的頭開始比較
    while(i <= mid && j <= right)
    {
        if (data[i] <= data[j])
        {
            temp[k++] = data[i++];
        }
        else
        {
            temp[k++] = data[j++];
        }
    }

    // 判斷哪個子陣列還有元素,並拷貝到 temp 後面
    while(i <= mid)
    {
        temp[k++] = data[i++];
    }
    while(j <= right)
    {
        temp[k++] = data[j++];
    }

    // 將 temp 中的資料拷貝到原陣列對應位置
    for(i = 0; i < k; i++)
    {
        data[left+i] = temp[i];
    }
}

1.2. 歸併排序演算法分析

  • 歸併排序是一個穩定的排序演算法,在進行子數組合並的時候,我們可以設定當元素大小相等時,先將前半部分的資料放入臨時陣列,這樣就可以保證相等元素在排序後依然保持原來的順序。

  • 不僅遞迴求解的問題可以寫成遞推公式,遞迴程式碼的時間複雜度也可以寫成遞迴公式

  • 如果我們對 n n 個元素進行歸併排序所需要的時間是 T ( n ) T(n) ,那分解成兩個子陣列排序的時間都是 T ( n 2 ) T(\frac{n}{2}) ,而合併兩個子陣列的時間複雜度為 O ( n ) O(n) 。所以,歸併排序的時間複雜度計算公式為:

T ( 1 ) = C T(1) = C
T ( n ) = 2 T ( n 2 ) + n , n &gt; 1 T(n) = 2*T(\frac{n}{2}) + n, n&gt;1

  • n = 1 時,只需要常量級的執行時間,所以表示為 C。

T ( n ) = 2 T ( n 2 ) + n T(n) = 2*T(\frac{n}{2}) + n
= 2 [ 2 T ( n 4 ) + n 2 ] + n = 4 T ( n 4 ) + 2 n = 2*[2*T(\frac{n}{4}) + \frac{n}{2}] + n = 4*T(\frac{n}{4}) + 2*n
= 4 [ 2 T ( n 8 ) + n 4 ] + 2 n = 8 T ( n 8 ) + 3 n = 4*[2*T(\frac{n}{8}) + \frac{n}{4}] + 2*n = 8*T(\frac{n}{8}) + 3*n
. . . . . . ......
= 2 k T ( n 2 k ) + k n = 2^k * T(\frac{n}{2^k}) + k * n
. . . . . . ......
n 2 k = 1 \frac{n}{2^k} = 1 時, k = l o g 2 n k = log_2n ,代入上式得:
T ( n ) = n C + n l o g 2 n T(n) = n * C + nlog_2n
用大 O 標記法來表示,歸併排序的時間複雜度為 O ( n l o g n ) O(nlogn)

  • 從我們的分析可以看出,歸併排序的執行效率與原始資料的有序程度無關,其時間複雜度是非常穩定的,不管是最好情況、最壞情況,還是平均情況,時間複雜度都是 O ( n l o g n ) O(nlogn)

  • 歸併排序有一個缺點,那就是它不是原地排序演算法。在進行子數組合並的時候,我們需要臨時申請一個數組來暫時存放排好序的資料。因為這個臨時空間是可以重複利用的,因此歸併排序的空間複雜度為 O ( n ) O(n) ,最多需要存放 n n 個數據。


2. 快速排序(Quick Sort)?

1.1. 快速排序演算法實現

  • 快速排序的思想是這樣的,如果要對陣列區間 [p, r] 的資料進行排序,我們先選擇其中任意一個數據作為 pivot(分支點),一般為區間最後一個元素。然後遍歷陣列,將小於 pivot 的資料放到左邊,將大於 pivot 的資料放到右邊。接著,我們再遞迴對左右兩邊的資料進行排序,直到區間縮小為 1 ,說明所有的資料都排好了序。
    快速排序
遞推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

終止條件:
p >= r
  • 歸併排序是由下向上的,先處理子陣列然後再合併。而快速排序正好相反,它的過程是由上向下的,先分出兩個子區間,再對子區間進行排序。歸併排序是穩定的時間複雜度為 O ( n ) O(n) ,但它是非原地演算法,而快排則是原地排序演算法。

歸併排序和快速排序

  • 快速排序的分割槽過程如下所示,從左到右依次遍歷陣列,如遇到小於 pivot 的元素,則進行資料交換 ,否則繼續往前進行,最後再放置 pivot。
    快排分割槽

  • 程式碼實現

// O(n(logn))
void Quick_Sort(float data[], int left, int right)
{
    if (left < right)
    {
        int i = left, j = left;
        int pivot = data[right];

        for (j = left; j < right; j++)
        {
            if (data[j] < pivot)
            {
                int temp = data[i];
                data[i] = data[j];
                data[j] = temp;
                i++;
            }
        }

        data[j] = data[i];
        data[i] = pivot;
        Quick_Sort(data, left, i-1);
        Quick_Sort(data, i+1, right);
    }
}
  • 快速排序的另一種實現方式如下所示,先取出一個元素作為 pivot(假設是最後一個),這時 pivot 位置可以看作為空,然後從左到右查詢第一個比 pivot 大的元素放在 pivot 的位置,此時空的地方變成了這第一個比 pivot 大的元素位置。然後從右到左查詢第一個比 pivot 小的元素放在剛才空的位置,依次迴圈直到從左到右和從右到左都查詢到了同一位置,這時候再把 pivot 放置在最後一個空位。這個過程可以形象的被稱為“挖坑填坑”。

快速排序