1. 程式人生 > >排序演算法之 歸併排序 及其時間複雜度和空間複雜度

排序演算法之 歸併排序 及其時間複雜度和空間複雜度

        在排序演算法中快速排序的效率是非常高的,但是還有種排序演算法的效率可以與之媲美,那就是歸併排序;歸併排序和快速排序有那麼點異曲同工之妙,快速排序:是先把陣列粗略的排序成兩個子陣列,然後遞迴再粗略分兩個子陣列,直到子數組裡面只有一個元素,那麼就自然排好序了,可以總結為先排序再遞迴;歸併排序:先什麼都不管,把陣列分為兩個子陣列,一直遞迴把陣列劃分為兩個子陣列,直到數組裡只有一個元素,這時候才開始排序,讓兩個陣列間排好序,依次按照遞迴的返回來把兩個陣列進行排好序,到最後就可以把整個陣列排好序;

演算法分析

        歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表
,稱為二路歸併
        基本思路:         先遞迴的把陣列劃分為兩個子陣列,一直遞迴到陣列中只有一個元素,然後再呼叫函式把兩個子陣列排好序,因為該函式在遞迴劃分陣列時會被壓入棧,所以這個函式真正的作用是對兩個有序的子陣列進行排序;         基本步驟:  1、判斷引數的有效性,也就是遞迴的出口;         2、首先什麼都不管,直接把陣列平分成兩個子陣列;         3、遞迴呼叫劃分陣列函式,最後劃分到陣列中只有一個元素,這也意味著陣列是有序的了;         4、然後呼叫排序函式,把兩個有序的數組合併成一個有序的陣列;         5、排序函式的步驟,讓兩個陣列的元素進行比較,把大的/小的元素存放到臨時陣列中,如果有一個數組的元素被取光了,那就直接把另一陣列的元素放到臨時陣列中,然後把臨時陣列中的元素都複製到實際的陣列中;

實現程式碼

#include<stdio.h>
 
 #define LEN 12   // 巨集定義陣列的大小
 static int tmp[LEN] = {0};// 設定臨時陣列
 
// 列印陣列
 void print_array(int *array)
 {
     int index = 0;
     printf("\narray:\n");
     for (; index < LEN; index++){
         printf(" %d, ", *(array + index));
     }
     printf("\n");
 }
 
// 把兩個有序的陣列排序成一個數組
 void _mergeSort(int *array, int start, int middle, int end)
 {
    int first = start;
    int second = middle + 1;
    int index = start;
    while ((first <= middle) && (second <= end)){
        if (array[first] >= array[second])
            tmp[index++] = array[second++];
        else
            tmp[index++] = array[first++];
    }   
    while(first <= middle) tmp[index++] = array[first++];
    while(second <= end) tmp[index++] = array[second++];
 
    for (first = start; first <= end; first++)
        array[first] = tmp[first];
 }

// 遞迴劃分陣列
 void mergeSort(int *array, int start, int end)
 {
     if (start >= end)
         return;
     int middle = ((end + start) >> 1);
     mergeSort(array, start, middle);// 遞迴劃分左邊的陣列
     mergeSort(array, middle+1, end);// 遞迴劃分右邊的陣列
     _mergeSort(array, start, middle, end);// 對有序的兩個陣列進行合併成一個有序的陣列
 }
 
 int main(void)
 {
     int array[LEN] = {2, 1, 4, 0, 12, 520, 2, 9, 5, 3, 13, 14};
     print_array(array);
     mergeSort(array, 0, LEN-1);
     print_array(array);
     return 0;
 }
        分析下上面程式碼:其實上面的程式碼主要的是兩個函式,第一個是劃分陣列函式,第二個是對兩個有序數組合並的歸併函式;這裡要藉助一個臨時陣列,有的人在main函式中申請動態陣列,然後讓所有遞迴呼叫都使用該陣列;也有的人在歸併函式裡申請個臨時陣列;而我的方法是定義一個全域性的臨時陣列;其實我感覺這幾個方法都是大同小異,因為不管是動態陣列還是全域性靜態陣列,在遞迴釋放呼叫排序函式時,都會儲存一份資料;如果是在排序函式中定義臨時陣列,那麼應該和前面的方法一樣的,因為是區域性臨時陣列,存放在棧空間,當該函式呼叫完後,會馬上釋放。所以我個人感覺這三種方法都差不多(如果是在歸併函式中定義的臨時陣列,則需要全部壓棧;而其他的就只需要壓入有用資料所佔的空間就可以) 執行結果:  

時間複雜度

        歸併的時間複雜度分析:主要是考慮兩個函式的時間花銷,一、陣列劃分函式mergeSort();二、有序陣列歸併函式_mergeSort();         _mergeSort()函式的時間複雜度為O(n),因為程式碼中有2個長度為n的迴圈(非巢狀),所以時間複雜度則為O(n);       簡單的分析下元素長度為n的歸併排序所消耗的時間 T[n]:呼叫mergeSort()函式劃分兩部分,那每一小部分排序好所花時間則為  T[n/2],而最後把這兩部分有序的數組合併成一個有序的陣列_mergeSort()函式所花的時間為  O(n);         公式:T[n]  =  2T[n/2] + O(n);         所以得出的結果為:T[n] = O( nlogn )         因為不管元素在什麼情況下都要做這些步驟,所以花銷的時間是不變的,所以該演算法的最優時間複雜度和最差時間複雜度及平均時間複雜度都是一樣的為:O( nlogn );好像有人說最差的時間複雜度不是O(nlogn),我不知道怎麼算出來的,知道的麻煩告知下,謝謝;

空間複雜度

        歸併的空間複雜度就是那個臨時的陣列和遞迴時壓入棧的資料佔用的空間:n + logn;所以空間複雜度為: O(n)

以時間換空間

        我看到網上很多blog分享空間複雜度只有O(1)的歸併排序法;因為傳統的歸併排序所消耗的空間主要是在歸併函式(把兩個有序的函式合併成一個有序的函式),所以如果要讓時間複雜度為 O(1)  ,那麼也只能在歸併函式中做文章了。程式碼就不列出來了,其主要思想就是藉助於快速排序(其實就是相當於歸併函式被快速排序函式替換了);這樣的方法雖然可以減少記憶體的消耗,但是卻會在時間上帶來損失,因為這樣時間複雜度卻變成了  O(n^2)  了;所以這種方法並不是一個兩全其美的idea;

總結

        歸併排序雖然比較穩定,在時間上也是非常有效的(最差時間複雜度和最優時間複雜度都為 O(nlogn)  ),但是這種演算法很消耗空間,一般來說在內部排序不會用這種方法,而是用快速排序;外部排序才會考慮到使用這種方法;         若有不正確之處,望大家指正,共同學習!謝謝!!!