1. 程式人生 > >歸並排序(轉)

歸並排序(轉)

mas 並發與並行 希爾 成了 不為 內存空間 con 繼續 內部實現

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

技術分享

合並排序最大的優點是它的時間復雜度為O(nlgn),這個是我們之前的選擇排序和插入排序所達不到的。他還是一種穩定性排序,也就是相等的元素在序列中的相對位置在排序前後不會發生變化。他的唯一缺點是,需要利用額外的N的空間來進行排序。

一 原理

技術分享

合並排序依賴於合並操作,即將兩個已經排序的序列合並成一個序列,具體的過程如下:

  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這兩個有序序列合並為一個大的序列的過程演示:

技術分享

二 實現

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

至上而下的合並是一種典型的分治算法(Divide-and-Conquer),如果兩個序列已經排好序了,那麽采用合並算法,將這兩個序列合並為一個大的序列也就是對大的序列進行了排序。

首先我們將待排序的元素均分為左右兩個序列,然後分別對其進去排序,然後對這個排好序的序列進行合並,代碼如下:

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個元素的數組為例,其調用堆棧為:

技術分享

我們單獨將Merge步驟拿出來,可以看到合並的過程如下:

技術分享

三 圖示及動畫

如果以排序38,27,43,3,9,82,10為例,將合並排序畫出來的話,可以看到如下圖:

技術分享

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

技術分享

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

技術分享

下圖演示了合並排序在不同的情況下的效率:

技術分享

四 分析

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的整數冪,這樣根據遞歸式我們可以畫出一棵樹:

技術分享

可以看到我們對數組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. 如果已經排好序了就不用合並了

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

技術分享

算法改動如下:

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的排序算法,並簡要對算法的復雜度進行的分析,希望本文對您理解合並排序有所幫助,下文將介紹快速排序算法。

轉自:http://www.cnblogs.com/yangecnu/p/Introduce-Merge-Sort.html

歸並排序(轉)