1. 程式人生 > >Java練習:分治法之合併排序(merge Sort)

Java練習:分治法之合併排序(merge Sort)

分而治之(divide-and-conquer)是一種古老但實用的策略、普適性的問題求解策略。本質上,分而治之策略是將整體分解成部分的思想。

按照系統科學的觀點,該策略僅適用於線性系統——整體正好對於部分之和

(兩路)合併排序遵循分治法的三個步驟,其操作如下:

(1) 分解:將陣列大致分成兩半。例如將9個元素的陣列劃分成5+4兩半。

(2) 排序:一般需要對子序列遞迴地再進行劃分。極端地,子序列僅僅剩下一個資料,則子序列不需要排序。以突出前後兩個步驟。實際程式,劃分是有度的。

(3) 合併:將兩個已排序的資料序列合併。

1.合併

[5.1.2 針對LinearList]的例程5-3歸併(merging)演算法

,解決的問題:已知la和lb的元素非降序排列(前面的元素小於等於後面的元素),將la和lb合併為lc,且lc中的元素非降序排列。

    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-21,要求將例程5-3修改成algorithm.recursion.Merging的static方法int[] merge(int[]arr1,int[] arr2)。例如
    /**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;
    }
練習10-22.:解釋arr3[k++] = arr2[j++]的等價程式碼,使用該技巧減少上一練習中程式碼的行數。
    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}.