淺入淺出數據結構(21)——合併排序

  在講解合併排序以前,咱們先來想想下面這個問題如何解決:算法

  有兩個數組A和B,它們都已各自按照從小到大的順序排好了數據,如今咱們要把它們合併爲一個數組C,且要求C也是按從小到大的順序排好,請問該怎麼作?數組

  這個問題很是容易解決,咱們將A、B和C都視爲隊列,而後不斷比較A和B的首部,取出其中更小的數據出隊,而後將該數據插入隊列C,若A或B有一方爲空了,則將另外一方數據順序出隊再插入C便可:spa

int i=0,j=0,z=0;
while(i<a_size && j<b_size)
{
    if(a[i]<b[j])
       c[z++]=a[i++];
    else
       c[z++]=b[j++];
}
while(i<a_size)
    c[z++]=a[i++];
while(j<b_size)
    c[z++]=b[j++];

  顯然,上述問題的解決很是簡單,時間複雜度爲O(N),N爲總數據量。也就是說,若是咱們有兩個已排好序的數組,那麼咱們就能夠快速地將它們合併起來,而這,就是合併排序的根本思想。code

  回顧快速排序,能夠看出快速排序的作法其實就是:選取樞紐,將小於樞紐的元素組成一個子數組,大於樞紐的元素組成一個子數組,只要這兩個子數組排好序,整個數組就能排好序,至於兩個子數組怎麼排好序,遞歸實現。blog

  合併排序的作法與快速排序有些相似,只是「過程」反了過來:將原數組對半分爲兩個子數組,只要這兩個子數組排好序,我就能將它們經過合併(上述問題的解法)來獲得有序的原數組,至於怎麼獲得兩個排好序的子數組,遞歸實現。排序

  用僞代碼來表示合併排序的過程,就是這樣:遞歸

void MergeSort(int *src,unsigned int left,unsigned int right)
{
if(left<right)
{
/*將原數組一分爲二*/ unsigned int center= (left+right)/2; /*經過遞歸實現子數組的排序*/ MergeSort(src,0,center); MergeSort(src,center+1,right);
/*僞代碼:將子數組合並*/
}
}

  有了僞代碼後,剩下的工做就是將僞代碼中的「空」填上去。接口

  上述僞代碼有兩個「空」,一個是實現遞歸的基準情形,這個「空」很好填,由於根本不用填,爲何呢?由於只要數組大小大於1,咱們就一直劃分下去,那麼最終劃分獲得的子數組將會只有1個數據,此時這個子數組必爲「有序」,也就是說遞歸的基準情形必然存在。隊列

  另外一個「空」則是實現子數組的合併,這個「空」咱們能夠參考本文一開始提出的問題的解法,可是該解法須要用到一個額外的數組C,且最終的有序數據都放進了C裏面,該怎麼辦呢?思路很簡單也很直接,那就是:既然你要一個額外數組,那我就給你一個額外數組tempArr,有序數據在tempArr裏面,而我但願它們在原數組裏面,那我就將tempArr裏的數據複製回來:內存

        /*將子數組合併到tempArr*/

        unsigned int i=left,j=center+1,z=left; //注意,i,j,z的初始化和範圍都要有所變化
        while(i<=center&&j<=right)
        {
            if(src[i]<src[j])
                tempArr[z++]=src[i++];
            else
                tempArr[z++]=src[j++];
        }
        while(i<=center)
            tempArr[z++]=src[i++];
        while(j<=right)
            tempArr[z++]=src[j++];
        
        /*將tempArr的數據拷貝到原數組中*/
        for (z = left;z <= right;++z)
            src[z] = tempArr[z];

 

 

   將上面的代碼填入到僞代碼中,並將僞代碼的參數稍加修改,便有了以下合併排序:

void MSort(int *src, int *tempArr, unsigned int left, unsigned int right)
{
    if (left < right)
    {
        /*將原數組一分爲二*/
        unsigned int center = (left + right) / 2;
        
        /*遞歸實現子數組排序*/
        MSort(src, tempArr, left, center);
        MSort(src, tempArr, center + 1, right);
        
        /*將子數組合併到tempArr*/
        unsigned int i=left,j=center+1,z=left;
        while(i<=center&&j<=right)
        {
            if(src[i]<src[j])
                tempArr[z++]=src[i++];
            else
                tempArr[z++]=src[j++];
        }
        while(i<=center)
            tempArr[z++]=src[i++];
        while(j<=right)
            tempArr[z++]=src[j++];
        
        /*將tempArr的數據拷貝到原數組中*/
        for (int i = left;i <= right;++i)
            src[i] = tempArr[i];
    }
}

  爲了方便調用,咱們再實現一個小接口:

void MergeSort(int *src, unsigned int size)
{
    int *tempArr = (int *)malloc(sizeof(int)*size);
    MSort(src, tempArr, 0, size - 1);
    free(tempArr);
}

  

  至此,合併排序就實現完畢了,其佔用的空間顯然比快速排序等算法要多出一倍,那麼其時間複雜度如何呢?咱們就來簡單的算算看。

  首先,咱們假設進行合併排序的數組大小爲N且爲2的冪,T(N)表示對其排序耗費的時間,那麼就有:

  1.T(1)=1

  2.T(N)=2*T(N/2)+2N(兩個N分別爲合併耗費時間和拷貝回原數組所耗費的時間)

  將2式左右除以N,得:

  T(N)/N=T(N/2)/(N/2)+2

  遞推該式,得:

  T(N/2)/(N/2)=T(N/4)/(N/4)+2

  T(N/4)/(N/4)=T(N/8)/(N/8)+2

  ……
  T(2)/2=T(1)/1+2

  將上述全部式子左側相加且右側相加,得:

  T(N)/N+T(N/2)/(N/2)+T(N/4)/(N/4)+……+T(2)/2=T(N/2)/(N/2)+T(N/4)/(N/4)+……+T(1)/1+2*logN

  化簡,得:

  T(N)/N=T(1)/1+2*logN,即T(N)=N+2*N*logN=O(N*logN)

  這個時間複雜度與快速排序的平均時間複雜度相同,比快速排序的最壞狀況要好得多,可是在實際應用中快速排序要比合並排序優先考慮,緣由在於合併排序須要更多的內存空間,而且從tempArr拷貝數據回原數組也是一項花費巨大的工做。

相關文章
相關標籤/搜索