一,歸並排序介紹
歸並排序是一個典型的基於分治的遞歸算法。它不斷地將原數組分成大小相等的兩個子數組(可能相差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二分查找思想。
五,參考資料