排序算法之歸併排序

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

相關文章
相關標籤/搜索