前面幾篇介紹的選擇排序、插入排序、冒泡排序等都是很是簡單很是基礎的排序算法,都是用了兩個for循環,時間複雜度是平方級別的。本篇介紹一個比前面稍微複雜一點的算法:歸併排序。歸併排序算法裏面的歸併思想和遞歸方法是值得咱們學習的,歸併的過程每每伴隨着遞歸,其餘不少地方都會用這兩種方法,好比前面一篇《劍指offer題目系列三》中第12題「合併兩個排序的鏈表」就用到這兩種思想方法。html
歸併的過程算法
對於兩個獨立的數組來講,是將兩個有序的數組合併到一個數組中,使合併後的數組依然有序。對於一個數組來講,能夠先將其劃分爲兩部分,先使其各部分都有序,而後合併成一個有序數組。具體操做時,先定義兩個指針,分別指向兩個數組中的元素,用於遍歷數組,而後新建一個數組用於存儲合併後的元素。數組
歸併排序中,假設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 兩個指針指向的兩個子數組的元素位置,灰色元素表明已遍歷完的元素,黑色加粗元素表明還未遍歷的元素。spa
歸併過程的代碼:設計
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++]; } }
下面介紹遞歸排序的兩種方式:自頂向下歸併排序和自底向上歸併排序,兩種方式都會用到上面的歸併代碼。指針
自頂向下歸併code
自頂向下歸併是一種基於遞歸方式的歸併,也是算法設計中「分治思想」的典型用法。它將一個大問題分割成一個個小問題,分別解決小問題,而後用全部小問題的答案來解決整個大問題。若是能將兩個子數組排序,就能經過歸併兩個子數組使整個數組排序。自頂向下歸併每次先將數組的左半部分排序,而後將右半部分排序,經過歸併左右兩部分使整個數組排序。詳細過程見下面代碼註釋。htm
自頂向下歸併完整代碼:blog
//歸併排序(遞歸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