歸並排序 詳解


  • 時間復雜度:O(nlogn)
  • 空間復雜度:O(N),歸並排序需要一個與原數組相同長度的數組做輔助來排序
  • 穩定性:歸並排序是穩定的排序算法,temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];這行代碼可以保證當左右兩部分的值相等的時候,先復制左邊的值,這樣可以保證值相等的時候兩個元素的相對位置不變。

 

歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。把長度為n的輸入序列分成兩個長度為n/2的子序列;對這兩個子序列分別采用歸並排序;將兩個排序好的子序列合並成一個最終的排序序列。

從下圖可以看出,每次合並操作的平均時間復雜度為O(n),而完全二叉樹的深度為|log2n|。總的平均時間復雜度為O(nlogn)。而且,歸並排序的最好,最壞,平均時間復雜度均為O(nlogn)。

 

 

 

圖示過程

(1) 歸並排序的流程

 

(2) 合並兩個有序數組的流程

 

 

 

 

歸並排序有兩種實現方式: 基於遞歸的歸並排序和基於循環的歸並排序。(也叫自頂向下的歸並排序和自底向上的歸並排序)

這兩種歸並算法雖然實現方式不同,但還是有共同之處的:

1. 無論是基於遞歸還是循環的歸並排序, 它們調用的核心方法都是相同的:完成一趟合並的算法,即兩個已經有序的數組序列合並成一個更大的有序數組序列 (前提是兩個原序列都是有序的!)

2. 從排序軌跡上看,合並序列的長度都是從小(一個元素)到大(整個數組)增長的

 

C++

一、基於遞歸的歸並排序(自頂向下的歸並排序)

void MergeSort (int arr [], int low,int high) {
    if(low>=high) { return; } // 終止遞歸的條件,子序列長度為1
    int mid =  low + (high - low)/2;  // 取得序列中間的元素
    MergeSort(arr,low,mid);  // 對左半邊遞歸
    MergeSort(arr,mid+1,high);  // 對右半邊遞歸
    merge(arr,low,mid,high);  // 合並
  }


void merge(int arr[],int low,int mid,int high){
    //low為第1有序區的第1個元素,i指向第1個元素, mid為第1有序區的最后1個元素
    int i=low, j=mid+1, k=0;  //mid+1為第2有序區第1個元素,j指向第1個元素
    int *temp=new int[high-low+1]; //temp數組暫存合並的有序序列
    while(i<=mid&&j<=high){
        if(arr[i]<=arr[j]) //較小的先存入temp中
            temp[k++]=arr[i++];
        else
            temp[k++]=arr[j++];
    }
    while(i<=mid)//若比較完之后,第一個有序區仍有剩余,則直接復制到t數組中
        temp[k++]=arr[i++];
    while(j<=high)//同上
        temp[k++]=arr[j++];
    for(i=low,k=0;i<=high;i++,k++)//將排好序的存回arr中low到high這區間
      arr[i]=temp[k];
    delete []temp;//釋放內存,由於指向的是數組,必須用delete []
}

 

基於遞歸歸並排序的優化方法

優化一:對小規模子數組使用插入排序

用不同的方法處理小規模問題能改進大多數遞歸算法的性能,因為遞歸會使小規模問題中方法調用太過頻繁,所以改進對它們的處理方法就能改進整個算法。因為插入排序非常簡單, 因此一般來說在小數組上比歸並排序更快。 這種優化能使歸並排序的運行時間縮短10%到15%。

怎么切換呢?只要把作為停止遞歸條件的

  if(low>=high) { return; }

改成

 if(high - low <= 10) { // 數組長度小於10的時候
      InsertSort(int arr[], int low,int high) // 切換到插入排序
      return;
 }

就可以了,這樣的話,這條語句就具有了兩個功能:

1. 在適當時候終止遞歸

2. 當數組長度小於M的時候(high-low <= M), 不進行歸並排序,而進行插排

 

優化二: 測試待排序序列中左右半邊是否已有序

通過測試待排序序列中左右半邊是否已經有序, 在有序的情況下避免合並方法的調用。

因為a[low...mid]和a[mid...high]本來就是有序的,存在a[low]<a[low+1]...<a[mid]和a[mid+1]<a[mid+2]...< a[high]這兩種關系, 如果判斷出a[mid]<=a[mid+1]的話,我們就認為數組已經是有序的並跳過merge() 方法。

void sort (int a[], int low,int high) {
    if(low>=high) {
      return;
    } // 終止遞歸的條件
    int mid =  low + (high - low)/2;  // 取得序列中間的元素
    sort(a,low,mid);  // 對左半邊遞歸
    sort(a,mid+1,high);  // 對右半邊遞歸
    if(a[mid]<=a[mid+1]) return; // 避免不必要的歸並
    merge(a,low,mid,high);  // 單趟合並
  }

優化三:去除原數組序列到輔助數組的拷貝

在上面介紹的基於遞歸的歸並排序的代碼中, 我們在每次調用merge方法時候,我們都把arr對應的序列拷貝到輔助數組temp中去。

遞歸調用的每個層次交換輸入數組和輸出數組的角色,從而不斷地把輸入數組排序到輔助數組,再將數據從輔助數組排序到輸入數組,節省數組復制的時間。

注意, 外部的sort方法和內部sort方法接收的a和aux參數剛好是相反的

 

在這里我們做了兩個操作:

  • 在排序前拷貝一個和原數組元素完全一樣的輔助數組(不再是創建一個空數組了!)
  • 在遞歸調用的每個層次交換輸入數組和輸出數組的角色

因為外部sort和merge的參數順序是相同的,所以,無論遞歸過程中輔助數組和原數組的角色如何替換,對最后一次調用的merge而言(將整個數組左右半邊合為有序的操作), 最終被排為有序的都是原數組,而不是輔助數組!

void MergeSortCore(int* data, int* copy, int first, int last);
void MergeSort(int* data, int length)
{
    if(data == NULL || length <= 0) return;
 
    int* copy = new int[length];
    for(int i = 0; i < length; i++)
        copy[i] = data[i];
 
    MergeSortCore(data, copy, 0, length - 1);
 
    for(int i = 0; i < length; i++)
        data[i] = copy[i];
    delete[] copy;
    return;
}
 
void MergeSortCore(int* data, int* copy, int first, int last)
{
    if(first == last)
        copy[first] = data[first];
    else
    {
        int mid = first + (last - first)/2;
        MergeSortCore(copy, data, first, mid);
        MergeSortCore(copy, data, mid + 1, last);
 
        //merge    
        int i = first, j = mid + 1;
        int k = first;
        while(i <= mid && j <= last)
        {
            if(data[i] <= data[j]) copy[k++] = data[i++];
            else copy[k++] = data[j++];
        }
 
        while(i <= mid) copy[k++] = data[i++];
        while(j <= last) copy[k++] = data[j++];
    }
}

 

注意:優化結果雖然差不多,但是當其數組接近有序的時候,速度有了可觀的提升。

 

 

 

二、基於循環的歸並排序(自底向上)

void sort(int a []){
    int N = a.size();
    for (int size = 1; size < N; size *= 2){
      //  low+size=mid+1,為第二個分區第一個元素,它 < N,確保最后一次合並有2個區間
      for(int low = 0; low + size < N;low += 2 * size) {
        mid = low + size - 1;
        high = low + 2 * size - 1;
        if(high > N-1) high = N - 1;
        merge(a,low, mid, high);
      }
    }
  }

 

 

 

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM