排序算法總結之歸並排序


一,歸並排序介紹

歸並排序是一個典型的基於分治的遞歸算法。它不斷地將原數組分成大小相等的兩個子數組(可能相差1),最終當划分的子數組大小為1時(下面代碼第17行left小於right不成立時) ,將划分的有序子數組合並成一個更大的有序數組。為什么是有序子數組???

歸並排序的遞歸公式:T(N) = 2T(N/2) + O(N)

從公式中可以看出:將規模為 N 的原問題分解成兩個規模 N/2 的兩個子問題;並且,合並這兩個子問題的代價是 O(N)---[后面的 +O(N) 表示合並的代價]

 

 

二,歸並排序算法分析

 歸並排序算法有兩個基本的操作,一個是,也就是把原數組划分成兩個子數組的過程。另一個是,它將兩個有序數組合並成一個更大的有序數組。

它將數組平均分成兩部分: center = (left + right)/2,當數組分得足夠小時---數組中只有一個元素時,只有一個元素的數組自然而然地就可以視為是有序的,此時就可以進行合並操作了。因此,上面講的合並兩個有序的子數組,是從 只有一個元素 的兩個子數組開始合並的。

合並后的元素個數:從 1-->2-->4-->8......

比如初始數組:[24,13,26,1,2,27,38,15]

①分成了兩個大小相等的子數組:[24,13,26,1]    [2,27,38,15]

②再划分成了四個大小相等的子數組:[24,13]   [26,1]    [2,27]    [38,15]

③此時,left < right 還是成立,再分:[24]   [13]   [26]    [1]    [2]     [27]    [38]   [15]

此時,有8個小數組,每個數組都可以視為有序的數組了!!!,每個數組中的left == right,從遞歸中返回(從19行--20行的代碼中返回),故開始執行合並(第21行):

merge([24],[13]) 得到 [13,24]

merge([26],[1]) 得到[1,26]

.....

.....

最終得到 有序數組。

 

三,歸並排序算法實現

 1 public class MergeSort {
 2 
 3     public static <T extends Comparable<? super T>> void mergeSort(T[] arr) {
 4         T[] tmpArray = (T[]) new Comparable[arr.length];
 5         mergeSort(arr, tmpArray, 0, arr.length - 1);
 6     }
 7 
 8     /**
 9      * 
10      * @param arr an array of Comparable items
11      * @param tmpArray an array to place the merge result
12      * @param left the left-most index of the array
13      * @param right right-most index of the array
14      */
15     private static <T extends Comparable<? super T>> void mergeSort(T[] arr,
16             T[] tmpArray, int left, int right) {
17         if (left < right) {
18             int center = (left + right) / 2;
19             mergeSort(arr, tmpArray, left, center);
20             mergeSort(arr, tmpArray, center + 1, right);
21             merge(arr, tmpArray, left, center + 1, right);
22         }
23     }
24 
25     /**
26      * 
27      * @param arr an array of Comparable items
28      * @param tmpArray an array to place the merge result
29      * @param leftPos the left-most index of the subarray
30      * @param rightPos the index of the start of the second half
31      * @param rightEnd the right-most index of the subarray
32      */
33     private static <T extends Comparable<? super T>> void merge(T[] arr,
34             T[] tmpArray, int leftPos, int rightPos, int rightEnd) {
35         int leftEnd = rightPos - 1;
36         int numElements = rightEnd - leftPos + 1;
37         int tmpPos = leftPos;// 只使用tmpArray中某一部分區域
38         while (leftPos <= leftEnd && rightPos <= rightEnd) {
39             if (arr[leftPos].compareTo(arr[rightPos]) <= 0)
40                 tmpArray[tmpPos++] = arr[leftPos++];
41             else
42                 tmpArray[tmpPos++] = arr[rightPos++];
43         }
44 
45         while (leftPos <= leftEnd)
46             tmpArray[tmpPos++] = arr[leftPos++];// copy rest of left half
47         while (rightPos <= rightEnd)
48             tmpArray[tmpPos++] = arr[rightPos++];// copy rest of right half
49 
50         // copy tmpArray back
51         for (int i = 0; i < numElements; i++, rightEnd--)
52             arr[rightEnd] = tmpArray[rightEnd];//只拷貝當前 merge 的部分數組
53 
54         /**
55          * 復制了整個數組中的所有元素 
56           for(int i = 0; i < tmpArray.length; i++)
57                  arr[i] = tmpArray[i];
58          */
59     }
60     
61     //for test purpose
62     public static void main(String[] args) {
63         Integer[] arr = {24,13,26,1,2,27,38,15};
64         mergeSort(arr);
65         for (Integer i : arr)
66             System.out.print(i + " ");
67     }
68 }

①第3行的公共方法,是對外的排序接口,首先創建一個臨時數組tmpArray,用來保存合並過程中,兩個子數組臨時合並的結果。將tmpArray作為參數傳遞給遞歸調用的方法,而不是在執行遞歸調用的方法里面創建臨時數組,這樣可以大大地減少臨時數組的創建。若在遞歸調用的方法里創建臨時數組,每一層遞歸調用,都會創建一個臨時數組。

②第15行的私有方法,是執行遞歸調用的方法。在某次具體的遞歸調用中,只用到了tmpArray中的某一部分空間(leftEnd 和 rightEnd之間的空間)。

③第38行while循環,比較兩個子數組中的元素,誰小就把誰放到tmpArray中。

 ④第45行和第47行的兩個while循環完成的功能是:當合並兩個有序的子數組時,一個子數組中的元素已經全部放到tmpArray中去了,另一個子數組中還剩下有元素,故將剩下的所有元素直接復制到tmpArray中。

⑤第51行for循環,將本次merge完成的兩個子數組復制到原數組中去。注意,它只復制本次參與合並的兩個子數組中的元素。為什么要復制到原數組中去呢?因為在下一次的合並過程中,需要合並的是更大的子數組,這個更大的數組,就是由上次合並的生成的有序小數組組成的。比如:

在合並這兩個數組時:[24]   [13]

下一次合並的則是:[13,24]  [1,26]

 

四,歸並排序算法復雜度分析

歸並排序中,用到了一個臨時數組,故空間復雜度為O(N)

由歸並排序的遞歸公式:T(N) = 2T(N/2) + O(N) 可知時間復雜度為O(NlogN)

數組的初始順序會影響到排序過程中的比較次數,但是總的而言,對復雜度沒有影響。平均情況 or 最壞情況下 它的復雜度都是O(NlogN)

此外,歸並排序中的比較次數是所有排序中最少的。原因是,它一開始是不斷地划分,比較只發生在合並各個有序的子數組時。

因此,JAVA的泛型排序類庫中實現的就是歸並排序。因為:對於JAVA而言,比較兩個對象的操作代價是很大的(根據Comparable接口的compareTo方法進行比較),而移動兩個對象,其實質移動的是引用,代價比較小。(排序本質上是兩種操作:比較操作和移動操作)

java.util.Arrays.sort(T[] arr)使用的是歸並排序

java.util.Arrays.sort(int[] arr) 使用的是快速排序

 

2018-11-24更新:

JDK7中使用 TimSort算法取代了原來的歸並排序,它結合了歸並排序和插入排序各自的優點,對歸並和插入做了優化,優化點:①歸並排序中的“分”不再划分到單個元素才停止;②插入排序比較時,引入binarySort二分查找思想。

五,參考資料

排序算法總結之插入排序

 排序算法總結之堆排序

排序算法總結之快速排序

各種排序算法的總結


免責聲明!

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



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