Java練習:分治法之合併排序(merge Sort)
分而治之(divide-and-conquer)是一種古老但實用的策略、普適性的問題求解策略。本質上,分而治之策略是將整體分解成部分的思想。
按照系統科學的觀點,該策略僅適用於線性系統——整體正好對於部分之和。
(兩路)合併排序遵循分治法的三個步驟,其操作如下:
(1) 分解:將陣列大致分成兩半。例如將9個元素的陣列劃分成5+4兩半。
(2) 排序:一般需要對子序列遞迴地再進行劃分。極端地,子序列僅僅剩下一個資料,則子序列不需要排序。以突出前後兩個步驟。實際程式,劃分是有度的。
(3) 合併:將兩個已排序的資料序列合併。1.合併
[5.1.2 針對LinearList]的例程5-3歸併(merging)演算法
練習10-21,要求將例程5-3修改成algorithm.recursion.Merging的static方法int[] merge(int[]arr1,int[] arr2)。例如public static void merge(LinearList la, LinearList lb,LinearList lc){ if(la==null || lb==null|| lc==null){ throw new java.lang.NullPointerException(); } lc.clear(); int i=0; int j=0; while( (i<=la.length())&&(j<=lb.length()) ){ int ai=la.getAt(i); int bj=lb.getAt(j); if(ai<=bj){ lc.add(ai); i++; }else{ lc.add(bj); j++; } } while( i<=la.length() ){ int ai=la.getAt(i++); lc.add(ai); } while( j<=lb.length() ){ int bj=lb.getAt(j++); lc.add(bj); } }
練習10-22.:解釋arr3[k++] = arr2[j++]的等價程式碼,使用該技巧減少上一練習中程式碼的行數。/**3 * 歸併(merging) * 把兩個非降序的陣列arr1和arr2,合併為元素非降序排列的陣列後返回。 */ public static int[] merge(int[] arr1,int[] arr2){ if(arr1==null || arr2==null){ throw new java.lang.NullPointerException(); } int len1 = arr1.length; int len2 = arr2.length; if(len1 == 0){return arr2;}//arr0={} if(len2 == 0){return arr1;} int i,j ; i =j = 0;//index of arr1 and arr2. int[] arr3 = new int[ len1 + len2 ]; //輔助陣列 int k =0 ;//index of arr3 //主迴圈,完成大體合併 while ( i<len1 && j<len2 ){//只要arr1而且arr2中還有元素 if( arr1[i] <= arr2[j] ){//比較兩種的第一個元素,將小的資料一個放入arr3 arr3[k] = arr1[i]; i++; }else{ arr3[k] = arr2[j]; j++; } k++; } //如果arr1或者arr2中還有元素未放入arr3 while(i<len1){ arr3[k] = arr1[i]; k++; i++; } while(j<len2){ arr3[k] = arr2[j]; k++; j++; } return arr3; }
public static int[] merge1(int[] arr1,int[] arr2){
if(arr1==null || arr2==null){
throw new java.lang.NullPointerException();
}
if(arr1.length == 0){return arr2;}//arr1={}
if(arr2.length == 0){return arr1;}
int[] arr3 = new int[ arr1.length + arr2.length ];
int i,j,k; i =j = k=0;//index of arr1,2,3
while ( i<arr1.length && j<arr2.length ){
if( arr1[i] <= arr2[j] ){
arr3[k++] = arr1[i++];
}else{
arr3[k++] = arr2[j++];
}
}
while( i<arr1.length )
arr3[k++] = arr1[i++];
while( j<arr2.length )
arr3[k++] = arr2[j++];
return arr3;
}
測試:
public static void test(){
int[] arr1 = {3,5,8,11};
int[] arr2 = {2,6,8,9,11,15,20};
int[] arr3 = merge1(arr1,arr2);
display(arr1);
display(arr2);
display(arr3);
}
private static void display(int[] data){//java.util.Arrays.toString(<span style="font-family: Arial, Helvetica, sans-serif;">data</span>)
for (int i:data){
System.out.print(" " + i);
}
System.out.println("");
}
方便遞迴
為了方便遞迴呼叫,將上面的arr1後接arr2,組成一個數組arr的一部分。low1、high1表示原arr1的起止點索引,low2、high2表示原arr2的起止點索引.(有low2其實可以不要high1)
練習10-22.:編寫public static void merge2(int[] arr, int low1, int high1, int low2, int high2)
public static void merge2(int[] arr, int low1, int high1, int low2, int high2){
//輔助空間arr3,arr3的長度為[low1,high2]即high2-low1+1
int[] arr3;
int k = 0; //輔助空間arr3的索引
int low3 = low1;
int high3 = high2;
arr3 = new int[high2-low1+1];
//合併。
while (low1 <= high1 && low2 <= high2){
if (arr[low1] < arr[low2]){
arr3[k++] = arr[low1++];
}else{
arr3[k++] = arr[low2++];
}
}
while(low1 <= high1){
arr3[k++] = arr[low1++];
}
while(low2 <= high2){
arr3[k++] = arr[low2++];
}
//最後將子序列arr3的有效元素放入初始的陣列arr
k=0;
for (int i = low3; i <= high3; i++){
arr[i] = arr3[k++];
}
} //end merge2()
精簡程式碼
以arr3為中心,將上面程式碼的while合併。
public static void merge3(int[] arr, int low1, int low2, int high2){
final int p = low1,q = low2-1,len =high2-low1 +1;
int[] arr3 = new int[len];
for(int k=0; k<len; k++){//輔助空間arr3的索引
if (
(low1 <= q && low2 <= high2 && arr[low1] <= arr[low2])
||(low2 > high2)//進一步
){
arr3[k] = arr[low1++];
}else{
arr3[k] = arr[low2++];
}
}
System.arraycopy(arr3, 0, arr,p,len);
} //end merge3()
最後還可以再精簡一下,if-else由?:替代。
public static void merge4(int[] arr, int low1, int low2, int high2){
final int p = low1,q = low2-1,len =high2-low1 +1;
int[] arr3 = new int[len];
for(int k=0; k<len; k++){//輔助空間arr3的索引
arr3[k] = (low1 <= q && low2 <= high2 && arr[low1] <= arr[low2]) ||(low2 > high2)?
arr[low1++]:arr[low2++];
}
System.arraycopy(arr3, 0, arr,p,len);
}
2.遞迴方法sort1(int[] arr, intfirst, int last)
(兩路)合併排序的分解,將陣列大致分成兩半,並遞迴分解到一個元素。因此分治法的三個步驟中分解、排序非常簡單。
package algorithm.recursion;
public class MergeSort{
public static int[] sort(int[] data){
sort1(data, 0, data.length-1);//為了遞迴,使用私有方法sort1()
return data;
}
private static void sort1(int[] arr, int first, int last){
//遞迴的結束條件:first>= last
if(first < last){
int middle = (first + last) / 2; //計算中間位置 (first + last) >>> 1;
sort1(arr, first, middle);//遞迴地呼叫
sort1(arr, middle +1, last);
Merging.merge4(arr, first,middle+1 , last);
}
}
}
注:由於algorithm.recursion.MergeSort作為第10章的內容,沒有歸檔到第11章排序中,所以MergeSort的int[] sort(int[] data)設計為static。它沒有作為algorithm.sorting.IntSort的子類。
3.改進
1、消除遞迴呼叫。algorithm.recursion.MergeSort三個步驟中分解、排序事實上沒有什麼作用。換言之,我們可以直接將待排序的陣列分成兩個元素一組,呼叫Merging.merge4,在n/2個長度為2的子集合上,兩兩進行Merging.merge4...
2.自然合併排序。掃描待排序的陣列,將整個陣列分解成一系列已經有序的陣列子段,然後合併、合併。如{2 6 4 7 8 0 3 9 1 5}可以分解成{2 6}{ 4 7 8}{ 0 3 9}{ 1 5}.