淺談算法和數據結構: 三 合併排序

原文: 淺談算法和數據結構: 三 合併排序

合併排序,顧名思義,就是經過將兩個有序的序列合併爲一個大的有序的序列的方式來實現排序。合併排序是一種典型的分治算法:首先將序列分爲兩部分,而後對每一部分進行循環遞歸的排序,而後逐個將結果進行合併。html

Definition of Merge Sort 

合併排序最大的優勢是它的時間複雜度爲O(nlgn),這個是咱們以前的選擇排序和插入排序所達不到的。他仍是一種穩定性排序,也就是相等的元素在序列中的相對位置在排序先後不會發生變化。他的惟一缺點是,須要利用額外的N的空間來進行排序。算法

一 原理

Merge_sort_animation2

合併排序依賴於合併操做,即將兩個已經排序的序列合併成一個序列,具體的過程以下:數組

  1. 申請空間,使其大小爲兩個已經排序序列之和,而後將待排序數組複製到該數組中。
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較複製數組中兩個指針所指向的元素,選擇相對小的元素放入到原始待排序數組中,並移動指針到下一位置
  4. 重複步驟3直到某一指針達到序列尾
  5. 將另外一序列剩下的全部元素直接複製到原始數組末尾

該過程實現以下,註釋比較清楚:數據結構

private static void Merge(T[] array, int lo, int mid, int hi)
{
    int i = lo, j = mid + 1;
    //把元素拷貝到輔助數組中
    for (int k = lo; k <= hi; k++)
    {
        aux[k] = array[k];
    }
    //而後按照規則將數據從輔助數組中拷貝回原始的array中
    for (int k = lo; k <= hi; k++)
    {
        //若是左邊元素沒了, 直接將右邊的剩餘元素都合併到到原數組中
        if (i > mid)
        {
            array[k] = aux[j++];
        }//若是右邊元素沒有了,直接將全部左邊剩餘元素都合併到原數組中
        else if (j > hi)
        {
            array[k] = aux[i++];
        }//若是左邊右邊小,則將左邊的元素拷貝到原數組中
        else if (aux[i].CompareTo(aux[j]) < 0)
        {
            array[k] = aux[i++];
        }
        else
        {
            array[k] = aux[j++];
        }
    }
}

下圖是使用以上方法將EEGMR和ACERT這兩個有序序列合併爲一個大的序列的過程演示:併發

Merge Step in Merge Sort

二 實現

合併排序有兩種實現,一種是至上而下(Top-Down)合併,一種是至下而上 (Bottom-Up)合併,二者算法思想差很少,這裏僅介紹至上而下的合併排序。ide

至上而下的合併是一種典型的分治算法(Divide-and-Conquer),若是兩個序列已經排好序了,那麼採用合併算法,將這兩個序列合併爲一個大的序列也就是對大的序列進行了排序。post

首先咱們將待排序的元素均分爲左右兩個序列,而後分別對其進去排序,而後對這個排好序的序列進行合併,代碼以下:學習

public class MergeSort<T> where T : IComparable<T>
{
    private static T[] aux; // 用於排序的輔助數組
    public static void Sort(T[] array)
    {
        aux = new T[array.Length]; // 僅分配一次
        Sort(array, 0, array.Length - 1);
    }
    private static void Sort(T[] array, int lo, int hi)
    {
        if (lo >= hi) return; //若是下標大於上標,則返回
        int mid = lo + (hi - lo) / 2;//平分數組
        Sort(array, lo, mid);//循環對左側元素排序
        Sort(array, mid + 1, hi);//循環對右側元素排序
        Merge(array, lo, mid, hi);//對左右排好的序列進行合併
    }
    ...
}

以排序一個具備15個元素的數組爲例,其調用堆棧爲:動畫

Top-Down merge的調用堆棧

咱們單獨將Merge步驟拿出來,能夠看到合併的過程以下:url

Trace of merge reuslt for top-down merge sort

三 圖示及動畫

若是以排序38,27,43,3,9,82,10爲例,將合併排序畫出來的話,能夠看到以下圖:

Merge_sort_algorithm_diagram

下圖是合併排序的可視化效果圖:

Merge Sort Visualization

對6 5 3 1 8 7 24 進行合併排序的動畫效果以下:

Merge-sort-example

下圖演示了合併排序在不一樣的狀況下的效率:

merge sort

四 分析

1. 合併排序的平均時間複雜度爲O(nlgn)

證實:合併排序是目前咱們遇到的第一個時間複雜度不爲n2的時間複雜度爲nlgn(這裏lgn表明log2n)的排序算法,下面給出對合並排序的時間複雜度分析的證實:

假設D(N)爲對整個序列進行合併排序所用的時間,那麼一個合併排序又能夠二分爲兩個D(N/2)進行排序,再加上與N相關的比較和計算中間數所用的時間。整個合併排序能夠用以下遞歸式表示:

D(N)=2D(N/2)+N,N>1;

D(N)=0,N=1; (當N=1時,數組只有1個元素,已排好序,時間爲0)

由於在分治算法中常常會用到遞歸式,因此在CLRS中有一章專門講解遞歸式的求解和證實,使用主定理(master theorem)能夠直接求解出該遞歸式的值,後面我會簡單介紹。這裏簡單的列舉兩種證實該遞歸式時間複雜度爲O(nlgn)的方法:

Prof1:處於方便性考慮,咱們假設數組N爲2的整數冪,這樣根據遞歸式咱們能夠畫出一棵樹:

merge sort analysis

能夠看到咱們對數組N進行MergeSort的時候,是逐級劃分的,這樣就造成了一個滿二叉樹,樹的每一及子節點都爲N,樹的深度即爲層數lgN+1,滿二叉樹的深度的計算能夠查閱相關資料,上圖中最後一層子節點沒有畫出來。這樣,這棵樹有lgN+1層,每一層有N個節點,因此

                             D(N)=(lgN+1)N=NlgN+N=NlgN

Prof2:咱們在爲遞歸表達式求解的時候,還有一種經常使用的方法就是數學概括法,

首先根據咱們的遞歸表達式的初始值以及觀察,咱們猜測D(N)=NlgN.

  1. 當N=1 時,D(1)=0,知足初始條件。
  2. 爲便於推導,假設N是2的整數次冪N=2k, 即D(2k)=2klg2k = k*2k
  3. 在N+1 的狀況下D(N+1)=D(2k+1)=2k+1lg2k+1=(k+1) * 2k+1,因此假設成立,D(N)=NlgN.

2. 合併排序須要額外的長度爲N的輔助空間來完成排序

若是對長度爲N的序列進行排序須要<=clogN 的額外空間,認爲就是就地排序(in place排序)也就是完成該排序操做須要較小的,固定數量的額外輔助內存空間。以前學習過的選擇排序,插入排序,希爾排序都是原地排序。

可是在合併排序中,咱們要建立一個大小爲N的輔助排序數組來存放初始的數組或者存放合併好的數組,因此須要長度爲N的額外輔助空間。固然也有前人已經將合併排序改造爲了就地合併排序,可是算法的實現變得比較複雜。

須要額外N的空間來輔助排序是合併排序的最大缺點,若是在內存比較關心的環境中可能須要採用其餘算法。

五 幾點改進

對合並排序進行一些改進能夠提升合併排序的效率。

1. 當劃分到較小的子序列時,一般可使用插入排序替代合併排序

對於較小的子序列(一般序列元素個數爲7個左右),咱們就能夠採用插入排序直接進行排序而不用繼續遞歸了),算法改造以下:

private const int CUTOFF = 7;//採用插入排序的閾值
private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //若是下標大於上標,則返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分數組
    Sort(array, lo, mid);//循環對左側元素排序
    Sort(array, mid + 1, hi);//循環對右側元素排序
    Merge(array, lo, mid, hi);//對左右排好的序列進行合併
}

2. 若是已經排好序了就不用合併了

當已排好序的左側的序列的最大值<=右側序列的最小值的時候,表示整個序列已經排好序了。

Stop if already sorted

算法改動以下:

private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //若是下標大於上標,則返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分數組
    Sort(array, lo, mid);//循環對左側元素排序
    Sort(array, mid + 1, hi);//循環對右側元素排序
   if (array[mid].CompareTo(array[mid + 1]) <= 0) return;
    Merge(array, lo, mid, hi);//對左右排好的序列進行合併
}

3. 並行化

分治算法一般比較容易進行並行化,在淺談併發與並行這篇文章中已經展現瞭如何對快速排序進行並行化(快速排序在下一篇文章中講解),合併排序同樣,由於咱們均分的左右兩側的序列是獨立的,因此能夠進行並行,值得注意的是,並行化也有一個閾值,當序列長度小於某個閾值的時候,中止並行化可以提升效率,這些詳細的討論在淺談併發與並行這篇文章中有詳細的介紹了,這裏再也不贅述。

六 用途

合併排序和快速排序同樣都是時間複雜度爲nlgn的算法,可是和快速排序相比,合併排序是一種穩定性排序,也就是說排序關鍵字相等的兩個元素在整個序列排序的先後,相對位置不會發生變化,這一特性使得合併排序是穩定性排序中效率最高的一個。在Java中對引用對象進行排序,Perl、C++、Python的穩定性排序的內部實現中,都是使用的合併排序。

七 結語

本文介紹了分治算法中比較典型的一個合併排序算法,這也是咱們遇到的第一個時間複雜度爲nlgn的排序算法,並簡要對算法的複雜度進行的分析,但願本文對您理解合併排序有所幫助,下文將介紹快速排序算法。

相關文章
相關標籤/搜索