排序算法之歸並排序


        前面幾篇介紹的選擇排序、插入排序、冒泡排序等都是非常簡單非常基礎的排序算法,都是用了兩個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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM