前面幾篇介紹的選擇排序、插入排序、冒泡排序等都是非常簡單非常基礎的排序算法,都是用了兩個for循環,時間復雜度是平方級別的。本篇介紹一個比前面稍微復雜一點的算法:歸並排序。歸並排序算法里面的歸並思想和遞歸方法是值得我們學習的,歸並的過程往往伴隨着遞歸,其他很多地方都會用這兩種方法,比如前面一篇《劍指offer題目系列三》中第12題“合並兩個排序的鏈表”就用到這兩種思想方法。
歸並的過程
對於兩個獨立的數組來說,是將兩個有序的數組合並到一個數組中,使合並后的數組依然有序。對於一個數組來說,可以先將其划分為兩部分,先使其各部分都有序,然后合並成一個有序數組。具體操作時,先定義兩個指針,分別指向兩個數組中的元素,用於遍歷數組,然后新建一個數組用於存儲合並后的元素。
歸並排序中,假設p、q、mid分別指向數組arr[]的第一個元素、最后一個元素、中間元素的索引位置,將數組arr[]划分成兩半:arr[p~mid]、arr[mid+1~q],然后將兩個子數組中的元素歸並。還可以將兩個子數組再次划分為更小的子數組,歸並更小的子數組……以此類推,直到子數組長度為1,然后依次歸並。歸並時,有4個判定條件:如果左半塊元素遍歷完畢,則直接將右半塊剩余元素放入數組中;如果右半塊元素遍歷完畢,則直接將左半塊剩余元素放入數組中;如果左半塊當前元素小於右半塊當前元素,則左半塊當前元素放入數組;反之,右半塊當前元素放入數組。
下面以長度為8的數組為例,說明歸並的具體過程。設原數組為int arr[] = {1,3,5,7,2,4,6,8};,新建一個輔助數組aux[]用於臨時存儲數組中的元素,先將原數組中的元素復制到輔助數組中,再把歸並的結果放回原數組中。初始i、j分別指向輔助數組前半部分、后半部分子數組的第一個元素位置,然后慢慢移動遍歷兩個數組。紅色元素代表每一趟 i、j 兩個指針指向的兩個子數組的元素位置,灰色元素代表已遍歷完的元素,黑色加粗元素代表還未遍歷的元素。
歸並過程的代碼:
public static void merge(int[] arr,int[] aux,int p,int mid,int q){ for(int k=p;k<=q;k++){ //先復制到輔助數組中 aux[k] = arr[k]; } int i=p,j=mid+1; //i、j指向輔助數組左右半塊指針,從起始位置開始 for(int k=p;k<=q;k++){ //k指向原數組arr[],根據i、j指針位置判斷左右半塊是否遍歷完 if(i > mid) arr[k] = aux[j++]; //左半塊遍歷完 else if(j>q) arr[k] = aux[i++]; //右半塊遍歷完 else if(aux[j]>aux[i]) arr[k] = aux[i++]; else arr[k] = aux[j++]; } }
下面介紹遞歸排序的兩種方式:自頂向下歸並排序和自底向上歸並排序,兩種方式都會用到上面的歸並代碼。
自頂向下歸並
自頂向下歸並是一種基於遞歸方式的歸並,也是算法設計中“分治思想”的典型用法。它將一個大問題分割成一個個小問題,分別解決小問題,然后用所有小問題的答案來解決整個大問題。如果能將兩個子數組排序,就能通過歸並兩個子數組使整個數組排序。自頂向下歸並每次先將數組的左半部分排序,然后將右半部分排序,通過歸並左右兩部分使整個數組排序。詳細過程見下面代碼注釋。
自頂向下歸並完整代碼:
//歸並排序(遞歸Recursion,自頂向下) public static void sort(int[] arr){ //本方法只會執行一次,下面兩個方法執行多次 if(arr == null) return; int[] aux = new int[arr.length]; //輔助數組 sort(arr,aux,0,arr.length-1); } public static void sort(int[] arr,int[] aux,int p,int q){ if(p>=q) return; int mid = (p+q)>>1; sort(arr,aux,p,mid); //左半塊歸並 sort(arr,aux,mid+1,q); //右半塊歸並 merge(arr,aux,p,mid,q); //歸並詳細過程 } public static void merge(int[] arr,int[] aux,int p,int mid,int q){ for(int k=p;k<=q;k++){ //先復制到輔助數組中 aux[k] = arr[k]; } int i=p,j=mid+1; //i、j指向輔助數組左右半塊指針,從起始位置開始 for(int k=p;k<=q;k++){ //k指向原數組arr[],根據i、j指針位置判斷左右半塊是否遍歷完 if(i > mid) arr[k] = aux[j++]; //左半塊遍歷完 else if(j>q) arr[k] = aux[i++]; //右半塊遍歷完 else if(aux[j]>aux[i]) arr[k] = aux[i++]; else arr[k] = aux[j++]; } }
自底向上歸並
上面自頂向下歸並是一種基於遞歸方式的歸並,解決大數組排序問題時很好用。實際上我們平時遇到的多數是小數組,所以自底向上歸並是先歸並那些微小數組,然后再成對歸並這些小數組,以此類推,直到將整個數組歸並在一起。首先我們進行的是兩兩歸並,然后是四四歸並,然后是八八歸並,一直進行下去。每趟最后一次歸並的第二個子數組長度可能比第一個子數組長度小,其余情況兩個子數組長度應該相等,每趟子數組長度翻倍。詳細過程見下面代碼注釋。
自底向上歸並完整代碼:
//非遞歸方式 public static void sortNotRecursion(int[] arr){ if(arr == null) return; int[] aux = new int[arr.length]; for(int i=1;i<arr.length;i*=2){ //p-q+1=2*i:即子數組長度為2*i,i為子數組半長,每趟i翻倍 for(int j=0;j<arr.length-i;j+=i*2){ //j:子數組起始位置 int p = j; //子數組頭指針 int q = Math.min(j+i*2-1,arr.length-1); //子數組尾指針,取兩者最小值僅僅是因為每一趟最后的子數組長度可能小於2*i,最后位置指針j+i*2-1的值可能會超過數組最大索引,此時取最大索引arr.length-1 int mid = j+i-1; //中間位置。注意不能用(p+q)>>1,因為每一趟最后的子數組長度可能小於2*i,q的位置可能是arr.length-1。 merge(arr,aux,p,mid,q); //每一趟最后一個子數組只有長度大於i時才會進行歸並操作,小於或等於i則不進行,由j<arr.length-i控制 } } } public static void merge(int[] arr,int[] aux,int p,int mid,int q){ for(int k=p;k<=q;k++){ //先復制到輔助數組中 aux[k] = arr[k]; } int i=p,j=mid+1; //i、j指向輔助數組左右半塊指針,從起始位置開始 for(int k=p;k<=q;k++){ //k指向原數組arr[],根據i、j指針位置判斷左右半塊是否遍歷完 if(i > mid) arr[k] = aux[j++]; //左半塊遍歷完 else if(j>q) arr[k] = aux[i++]; //右半塊遍歷完 else if(aux[j]>aux[i]) arr[k] = aux[i++]; else arr[k] = aux[j++]; } }
歸並排序是一種穩定的排序算法,但它不是原地歸並,而是需要一個輔助數組。歸並排序的時間復雜度為O(NlogN),空間復雜度為O(N)。
轉載請注明出處 http://www.cnblogs.com/Y-oung/p/8964964.html
工作、學習、交流或有任何疑問,請聯系郵箱:yy1340128046@163.com