1. 程式人生 > >排序算法(四)——歸並排序與遞歸

排序算法(四)——歸並排序與遞歸

display end 排序算法 while led 最大 nts erb merge

基本思想

分析歸並排序之前。我們先來了解一下分治算法

分治算法的基本思想是將一個規模為N的問題分解為K個規模較小的子問題。這些子問題相互獨立且與原問題性質相同。求出子問題的解。就可得到原問題的解。

分治算法的一般步驟:

(1)分解,將要解決的問題劃分成若幹規模較小的同類問題;

(2)求解,當子問題劃分得足夠小時,用較簡單的方法解決。

(3)合並。按原問題的要求,將子問題的解逐層合並構成原問題的解。

?

歸並排序是分治算法的典型應用。

歸並排序先將一個無序的N長數組切成N個有序子序列(僅僅有一個數據的序列覺得是有序序列),然後兩兩合並。再將合並後的N/2(或者N/2 + 1)個子序列繼續進行兩兩合並,以此類推得到一個完整的有序數組。

步驟例如以下圖所看到的:

技術分享圖片


java實現

歸並排序的核心思想是將兩個有序的數組歸並到還有一個數組中,所以須要開辟額外的空間

第一步要理清歸並的思路。假設如今有兩個有序數組A和B,要將兩者有序地歸並到數組C中。

我們用一個實例來推演:

技術分享圖片

上圖中。A數組中有四個元素,B數組中有六個元素,首先比較A、B中的第一個元素,將較小的那個放到C數組的第一位,由於該元素就是A、B全部元素中最小的。上例中。7小於23,所以將7放到了C中。

然後,用23與B中的其它元素比較。假設小於23,繼續按順序放到C中;假設大於23。則將23放入C中。

23放入C中之後。用23之後的47作為基準元素,與B中的其它元素繼續比較,反復上面的步驟。

假設有一個數組的元素已經全部拷貝到C中了,那麽將還有一個數組中的剩余元素依次插入C中就可以。至此結束。

依照上面的思路,用java實現:

/**
    * 歸並arrayA與arrayB到arrayC中
    * @param arrayA  待歸並的數組A
    * @param sizeA 數組A的長度
    * @param arrayB  待歸並的數組B
    * @param sizeB 數組B的長度
    * @param arrayC  輔助歸並排序的數組
    */
   public static void merge(int [] arrayA,int sizeA,
                         int [] arrayB,int sizeB,
                         int [] arrayC){
      
       int i=0,j=0,k=0;  //分別當作arrayA、arrayB、arrayC的下標指針
             
       while(i<sizeA&& j<sizeB){  //兩個數組都不為空
          if(arrayA[i]<arrayB[j]){//將兩者較小的那個放到arrayC中
              arrayC[k++]= arrayA[i++]; 
          }else{
              arrayC[k++]= arrayB[j++];
          }
       }  //該循環結束後。一個數組已經全然拷貝到arrayC中了,還有一個數組中還有元素
      
       //後面的兩個while循環用於處理還有一個不為空的數組
       while(i<sizeA){ 
          arrayC[k++]= arrayA[i++]; 
       }
      
       while(j<sizeB){ 
          arrayC[k++]= arrayA[j++];
       }
      
       for(intl=0;l<arrayC.length;l++){  //打印新數組中的元素
          System.out.print(arrayC[l]+"\t");
       }
   }

?再歸並之前,還有一步工作須要提前做好,就是數組的分解,能夠通過遞歸的方法來實現。遞歸(Recursive)是算法設計中經常使用的思想。

這樣通過先遞歸的分解數組,再合並數組就完成了歸並排序。

完整的java代碼例如以下:

public class Sort {
 
   private int [] array;  //待排序的數組
  
   public Sort(int [] array){
       this.array= array;
   }
  
   //按順序打印數組中的元素
   public void display(){
       for(int i=0;i<array.length;i++){
          System.out.print(array[i]+"\t");
       }
       System.out.println();
   }
 
   //歸並排序
   public void mergeSort(){
      
       int[] workSpace = new int [array.length]; //用於輔助排序的數組
       recursiveMergeSort(workSpace,0,workSpace.length-1);
   }
  
   /**
    * 遞歸的歸並排序
    * @param workSpace  輔助排序的數組
    * @param lowerBound 欲歸並數組段的最小下標
    * @param upperBound 欲歸並數組段的最大下標
    */
   private void recursiveMergeSort(int [] workSpace,int lowerBound,int upperBound){
      
       if(lowerBound== upperBound){  //該段僅僅有一個元素,不用排序
          return;
       }else{
          int mid = (lowerBound+upperBound)/2;
          recursiveMergeSort(workSpace,lowerBound,mid);    //對低位段歸並排序
          recursiveMergeSort(workSpace,mid+1,upperBound);  //對高位段歸並排序
          merge(workSpace,lowerBound,mid,upperBound);
          display(); 
       }
   }
 
   /**
    * 對數組array中的兩段進行合並。lowerBound~mid為低位段,mid+1~upperBound為高位段
    * @param workSpace 輔助歸並的數組,容納歸並後的元素
    * @param lowerBound 合並段的起始下標
    * @param mid 合並段的中點下標
    * @param upperBound 合並段的結束下標
    */
   private void merge(int [] workSpace,int lowerBound,int mid,int upperBound){
      
       int lowBegin = lowerBound;  //低位段的起始下標
       int lowEnd = mid;           //低位段的結束下標
       int highBegin = mid+1;  //高位段的起始下標
       int highEnd = upperBound;  //高位段的結束下標
       int j = 0; //workSpace的下標指針
       int n = upperBound-lowerBound+1;  //歸並的元素總數
      
       while(lowBegin<=lowEnd && highBegin<=highEnd){ 
          if(array[lowBegin]<array[highBegin]){//將兩者較小的那個放到workSpace中
              workSpace[j++]= array[lowBegin++]; 
          }else{
              workSpace[j++]= array[highBegin++]; 
          }
       } 
      
       while(lowBegin<=lowEnd){ 
          workSpace[j++]= array[lowBegin++]; 
       }
      
       while(highBegin<=highEnd){ 
          workSpace[j++]= array[highBegin++]; 
       }
      
       for(j=0;j<n;j++){  //將歸並好的元素拷貝到array中
          array[lowerBound++]= workSpace[j];
       }
      
   }
}
用以下代碼測試:

int [] a ={6,2,7,4,8,1,5,3};
Sort sort = newSort(a);
sort.mergeSort();

打印結果例如以下:

技術分享圖片


歸並的順序是這種:先將初始數組分為兩部分。先歸並低位段,再歸並高位段。對低位段與高位段繼續分解,低位段分解為更細分的一對低位段與高位段,高位段相同分解為更細分的一對低位段與高位段。依次類推。

上例中,第一步,歸並的是6與2,第二步歸並的是7和4。第三部歸並的是前兩步歸並好的子段[2,6]與[4,7]。至此,數組的左半部分(低位段)歸並完成,然後歸並右半部分(高位段)。

所以第四步歸並的是8與1,第四部歸並的是5與3,第五步歸並的是前兩步歸並好的字段[1,8]與[3,5]。

至此,數組的右半部分歸並完成。

最後一步就是歸並數組的左半部分[2,4,6,7]與右半部分[1,3,5,8]。

歸並排序結束。

?

在本文開始對歸並排序的描寫敘述中。第一躺歸並是對全部相鄰的兩個元素歸並結束之後,才進行下一輪歸並,並非先歸並左半部分,再歸並右半部分。可是程序的運行順序與我們對歸並排序的分析邏輯不一致。所以理解起來有些困難。

以下結合代碼與圖例來具體分析一下歸並排序的過程。

虛擬機棧(VM ?Stack)是描寫敘述Java方法運行的內存模型,每一次方法的調用都伴隨著一次壓棧、出棧操作。

我們要排序的數組為:

int [] a = {6,2,7,4,8,1,5,3}

?

當main()方法調用mergeSort()方法時,被調用的方法被壓入棧中,然後程序進入mergeSort()方法:

public void mergeSort(){
       int[] workSpace = new int [array.length]; //用於輔助排序的數組
       recursiveMergeSort(workSpace,0,workSpace.length-1);
   }
此時,mergeSort()又調用了recursiveMergeSort(workSpace,0,7)方法,recursiveMergeSort(workSpace,0,7)方法也被壓入棧中,在mergeSort()之上。

然後,程序進入到recursiveMergeSort(workSpace,0,7)方法: ? ??

 if(lowerBound== upperBound){  //該段僅僅有一個元素,不用排序
          return;
       }else{
          int mid = (lowerBound+upperBound)/2;
          recursiveMergeSort(workSpace,lowerBound,mid);    //對低位段歸並排序
          recursiveMergeSort(workSpace,mid+1,upperBound);  //對高位段歸並排序
          merge(workSpace,lowerBound,mid,upperBound);
          display(); 
       }

?lowerBound參數值為0,upperBound參數值為7,不滿足lowerBound== upperBound的條件。所以方法進入else分支,然後調用方法recursiveMergeSort(workSpace,0,3) ,

recursiveMergeSort(workSpace,0,3)被壓入棧中,此時棧的狀態例如以下:

技術分享圖片

然而,recursiveMergeSort(workSpace,0,3)不能馬上返回。它在內部又會調用recursiveMergeSort(workSpace,0,1),recursiveMergeSort(workSpace,0,1)又調用了recursiveMergeSort(workSpace,0,0),此時,棧中的狀態例如以下:

技術分享圖片

程序運行到這裏。終於有一個方法能夠返回了結果了——recursiveMergeSort(workSpace,0,0),該方法的運行的邏輯是對數組中的下標從0到0的元素進行歸並。該段僅僅有一個元素,所以不用歸並。馬上return。

方法一旦return。就意味著方法結束,recursiveMergeSort(workSpace,0,0)從棧中彈出。這時候,程序跳到了代碼片段(二)中的第二行:

recursiveMergeSort(workSpace,1,1);

該方法入棧。與recursiveMergeSort(workSpace,0,0)相似,不用歸並,直接返回。方法出棧。

這時候程度跳到了代碼片段(二)中的第三行:

merge(workSpace,0,0,1);

即對數組中的前兩個元素進行合並(自然,merge(workSpace,0,0,1)也伴隨著一次入棧與出棧)。

至此,代碼片段(二)運行完成,recursiveMergeSort(workSpace,0,1)方法出棧,程序跳到代碼片段(三)的第二行:

recursiveMergeSort(workSpace,2,3);

該方法是對數組中的第三個、第四個元素進行歸並。與運行recursiveMergeSort(workSpace,0,1)的過程相似,終於會將第三個、第四個元素歸並排序。

然後,程序跳到程序跳到代碼片段(三)的第三行:

merge(workSpace,0,1,3);

將前面已經排好序的兩個子序列(第一第二個元素為一組、第三第四個元素為一組)合並。

然後recursiveMergeSort(workSpace,0,3)出棧,程序跳到代碼片段(四)的第二行:

recursiveMergeSort(workSpace,4,7);

對數組的右半部分的四個元素進行歸並排序,伴隨著一系列的入棧、出棧,最後將後四個元素排好。

此時,數組的左半部分與右半部分已經有序。

然後程序跳到代碼片段(四)第三行:

merge(workSpace,0,3,7);

對數組的左半部分與右半部分合並。

然後recursiveMergeSort(workSpace,4,7)出棧,mergeSort()出棧,最後main()方法出棧。程序結束。


算法分析

先來分析一下復制的次數。

假設待排數組有8個元素。歸並排序須要分3層,第一層有四個包括兩個數據項的自數組,第二層包括兩個包括四個數據項的子數組,第三層包括一個8個數據項的子數組。合並子數組的時候,每一層的全部元素都要經歷一次復制(從原數組拷貝到workSpace數組),復制總次數為3*8=24次,即層數乘以元素總數。

設元素總數為N,則層數為log2N。復制總次數為N*log2N。

事實上,除了從原數組拷貝到workSpace數組。還須要從workSpace數組拷貝到原數組,所以。終於的復制復制次數為2*N*log2N。

在大O表示法中,常數能夠忽略。所以歸並排序的時間復雜度為O(N* log2N)。

一般來講。復制操作的時間消耗要遠大於比較操作的時間消耗,時間復雜度是由復制次數主導的。

?

以下我們再來分析一下比較次數。

在歸並排序中,比較次數總是比復制次數少一些。

如今給定兩個各有四個元素的子數組。首先來看一下最壞情況和最好情況下的比較次數為多少。

技術分享圖片

第一種情況。數據項大小交錯,所以必須進行7次比較,另外一種情況中,一個數組比還有一個數組中的全部元素都要小,因此僅僅須要4次比較。

當歸並兩個子數組時。假設元素總數為N。則最好情況下的比較次數為N/2,最壞情況下的比較次數為N-1。

假設待排數組的元素總數為N。則第一層須要N/2次歸並。每次歸並的元素總數為2;則第一層須要N/4次歸並。每次歸並的元素總數為4;則第一層須要N/8次歸並,每次歸並的元素總數為8……最後一次歸並次數為1,歸並的元素總數為N。總層數為log2N。

最好情況下的比較總數為:

N/2*(2/2)+ N/4*(4/2)+N/8*(8/2)+...+1*(N/2) = (N/2)*log2N

最好情況下的比較總數為:

N/2*(2-1)+ N/4*(4-1)+N/8*(8-1)+...+1*(N-1) =

(N-N/2)+ (N-N/4)+(N-N/8)+...+(N-1)=

N*log2N-(1+N/2+N/4+..)< N*log2N

可見,比較次數介於(N/2)*log2N與N*log2N之間。假設用大O表示法。時間復雜度也為O(N* log2N)。

排序算法(四)——歸並排序與遞歸