2.2 歸并排序(分類,合并)排序

核心思想:將一個數組進行排序,可以先(遞歸)將他分成兩半分別排序,然后把結果合并起來.若將兩個有序表合并成一個有序表,稱為二路歸并
即:將數組分成兩部分,左邊部分,和右邊部分分別遞歸再次分成兩部分知道每個數組里面只有一個元素.然后調用合并函數,將兩個數組進行排好序.每次排序的時候會使用同一個臨時緩存,儲存排好序的元素.最后的排序結果是 依次把 左半邊的 數組 兩兩排序合并,遞歸再把合并的數組繼續兩兩合并,最后左邊的數組全部合并;接下來對右半部分的數組兩兩排序合并,重復左邊的類似動作,把右邊的數組排序成一個數組;最后進行左右兩個有序數組合并,組成最終有序數組.
歸并排序的時間復雜度是NlogN,缺點是所需要的額外輔助空間和N成正比(需要分配一個與N長度一致的臨時存儲空間).

2.2.1原地歸并的抽象方法:

因為考慮到直接歸并算法每次合并需使用一個臨時空間,所以考慮在原始數組上直接進行排序,先對前半部分排序,在對后半部分排序.但是相對二路歸并的復雜度高.

2.2.2二路歸并具體的算法思路:(是一種自頂向下的mergesort)

1.判斷需要進入遞歸操作的邊界值.(一般 left<right)
2.把數組分成兩半. Mid =(left+right)/2
3.進行左半邊數組遞歸調用.直到 left=right ,不在遞歸.此時提取出數組的第一個元素
4.進行右半邊數組遞歸調用.
5.調用merge方法,把兩個有序數組合并.合并的時候會使用一個臨時數組.合并的思路是從兩個有序的數組第一個元素開始,依次比較,把數據按從小到大的順序存放到臨時temp;遇到某一個數組完成了,剩下一個數組還沒比較完,則把剩下的數組復制到temp尾部.最后把temp數組讀取,替換實際數組中.
步驟如下圖:
要把a[]分成a[lo...hi]兩部分,進行分別對 a[lo...mid] ,a[mid+1...hi]進行遞歸調用,直到調用的數組lo和hi相同時跳出,繼而進行和右邊部分合并排序.


image.png

代碼如下:

public class Mergesort{
    public static int [] temp;
    public void sort(int[] arr){
    if(arr==null||arr.lenth==0){
         return;}
        temp=new int[arr.lenth];
         mergesort(arr,0,arr.lenth-1);
    }
        private void mergsort(int[] arr,int low,int high){
        if(low>=high){return;}
       int mid=(low+high)/2;
       mergesort(arr,low,mid);
      mergesort(mid+1,high);
     merge(arr,low,mid,high);
 }
 private void merge(int[] arr,int low,int mid,int high){
    int leftIndex=low;
    int  rightIndex=mid+1;
    int tempIndex=0;
    while(leftIndex<=mid&&rightIndex<=high){
     if(arr[leftIndex]<arr[rightIndex]){
        temp[tempIndex++]=arr[leftIndex++];
         }else{
        temp[tempIndex++]=arr[rightIndex++]; 
    }
//已經比較完了,接下來如果發現左邊 或者右邊還有沒到最后的部分,則把那一部分直接添加到temp尾部
    while(leftIndex<=mid){
    temp[tempIndex++]=arr[leftIndex++];
    }   
        while(rightIndex<=high){
    temp[tempIndex++]=arr[rightIndex++];
    }   
//把temp 合并到arr中
    int t=0;
    while(low<=high){
        arr[low++]=temp[t++];
    }
}
 }
}
時間復雜度計算:

因為遞歸算法的時間復雜度可以表示為T(n)=2T(n/2)+O(n).
這是一個每次分一半的遞歸,總共執行排序的步驟可以畫成一棵類似的樹.

image.png

因為每一層的子問題時間代價為cn,一共有log2N+1 層,所以所有的時間=cn(lgN+1)=cnlnN+cn 時間復雜度
O(NlgN).
命題F:
對于長度為N的任意數組,自頂向下歸并排序的需要 ?NlgN ≤O≤ NlgN次比較.
命題G:
對于長度為N的任意數組,自頂向下歸并排序最多要訪問數組6NlgN次.
證明:
每次歸并最多需要訪問數組6N次(2N 復制,2N用來將排好序的數組復制過去,2N次進行合并),根據命題F,可知需要有NlgN次比較,所以訪問數組為6NlgN.

2.2.3對自定向下的歸并排序的優化

1.對小規模的數組使用插入排序.(具體設置的閥值7) 一般插入排序處理小規模排序(例如15)可以將歸并縮短時間10%~15%
2.測試數組是否已經有序.在merge之前添加一個方法判斷arr[mid]≤arr[mid+1],如果是的話說明這個從lo ...high的數組已經是有序了
3.在merge的時候不進行多次復制.(不把數組復制到臨時數組,然后又進行把臨時數組的數據copy到src中,這里面至少要經過2次重復操作)優化對臨時數組的空間上面還是保持不變,主要在與減少時間.
代碼如下:

public class MergeSort{
    private static final int OFF_CUT=7;
    public void sort(int[] src){
        int[] org=src.clone();
        mergeSort(org,src,0,org.length-1);
    }
    private void mergeSort(int[] src,int[] dst,int lo,int hi){
     //進行閥值判斷
        if(hi<lo+OFF_CUT){
            insertSort(dst,lo,hi);
            return ;
        }
        int mid=(lo+hi)/2;
        mergeSort(dst,src,lo,mid);
        mergeSort(dst,src,mid+1,hi);
        //如果src已經有序,那么直接復制到dst即可
        if(src[mid+1]>=src[mid]){
            System.arrayCopy(src,lo,dst,lo,hi-lo+1);
        }
        merge(src,dst,lo,mid,hi);
    }
    private void insertSort(int[] arr,int lo,int hi){

        for(int i=lo+1;i<=hi;i++){
            int temp=arr[i];
            int j=i;
            while(j>lo&&temp<arr[j-1];j--){
                arr[j]=arr[j-1];
            }
            arr[j]=temp;
        }
    }

    private void merge(int[] src,int[] dst,int low,int mid,int hi){
        int lowIndex=low;
        int hiIndex=mid+1;
        for(int i=low;i<=hi;i++){
            if(lowIndex>mid){
                dst[i]=src[hiIndex++];
            }else if(hiIndex>hi){
                dst[i]=src[lowIndex++];
            }else if (src[lowIndex]<src[hiIndex]) {
                dst[i]=src[lowIndex++];
            }else{
                dst[i]=src[hiIndex++];
            }
        }
    }
}
2.2.4自底向上的歸并排序

使用歸并排序的另一種方法是 先歸并那些微型數組,然后在歸并得到的子數組,反復;直到我們得到整個數組并歸并一起.這樣比標準遞歸方法需要的代碼更少.
[手搖算法,又稱三次反轉]
用于反轉字符串包括里面有不服順序改變,可以將空間復雜度降低至O(1).
具體做法.例子:
題目要求部分反轉數組。比如說1,2,3,4,5 翻轉后是3,4,5,1,2

class Reverse{
private void reverse(int[] a, int low, int hi) {
    while (low < hi) {
        swap(a, low, hi);
        low++;
        hi--;
    }
}

//這里也用到內存反轉3次 進行交換兩個數 ,采用 ^操作 
private void swap(int[] a, int left, int right) {
    a[left] ^= a[right];
    a[right] ^= a[left];
    a[left] ^= a[right];
}
public static void main(String[] args) {
    int[] test = {1, 2, 3, 4, 5};
    Reversel rev=new Reverse();
    rev.reverse(test, 0, 1);
    rev.reverse(test, 2, test.length - 1);
    rev.reverse(test, 0, test.length - 1);
    for (int i = 0; i < test.length; i++) {
        System.out.print(test[i]);
    }
}
}
自底向上,的歸并排序的思路是
  • 先分割成前半部分為1,后半部分的數組, 劃分成 N/2 段.
  • 然后接著以分成前半部分為2,后半部分為2的數組.
  • 然后接著以分成前半部分為3,后半部分為3的數組.
  • 直到每部分的步長接近N 或者超過N.
/**
 * Created by leon on 18-1-24.
 * 這里是采用自底向上的算法
 */
public class MergeSortBottom2Up {
    public void sort(int[] arr) {
        System.out.println("自底向上遞歸前:" + Arrays.toString(arr));
        int num = arr.length;
        //翻倍遞增
        for (int i = 1; i < num; i = i + i) {
            for (int j = 0; j < num; j++) {
                int low = Math.min(j, num - 1);
                j = j + i + i - 1;/*往后移動步長*/
                int high = Math.min(j, num - 1);
                int mid = (low + high) / 2;
                merge(arr, low, mid, high);
            }
        }
        System.out.println("自底向上遞歸后:" + Arrays.toString(arr));
    }
    /**
     * 進行在一個數組里面for循環,發現lowIndex下標超過mid 或者 highIndex 超過high進行切換
     * 比較的思路:
     * 1.lowIndex=lo,midIndex,highIndex=mid+1
     * 1.從lowIndex 開始 ++,比較src[lowIndex] 和 src[highIndex]的值,直到    src[lowIndex]>src[highIndex]停止
     * 2.用midIndex 定位到highIndex
     * 3.從highIndex++,直到src[highIdex]>src[lowIndex],停
     * 此刻說明從 midIndex~highIndex-1的數比lowIndex的小,需要插入到lowIndex之前.
     * 利用內存3次反轉(手搖技術)反轉內存,達到把midIndex~highIndex-1部分插入到lowIndex之前
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * `^``````````^`^``````
     *  l            h    =>初始 l,m,h=m+1
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * ```^````````^`^````
     *    l          h   =>移動l,arr[l]>arr[h]
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     * ```l``````````m`h``` =>把m->h位置,h++,直到arr[h]>arr[l]
     *此時說明 arr[m]~arr[h-1]的數小于 arr[l],需要進行3次內存反轉,
     *達到把arr[m]~arr[h-1]排到arr[lowIndex]前面
     * ____________________
     * |1|3|4|5|9|10|2|6|7|
     *
     * ____________________
     * |1|10|9|5|4|3|2|6|7|==> 反轉左邊
          ^          ^ ^
     * ____________________
     * |1|10|9|5|4|3|2|6|7|==>反轉右邊 只有一個 2
     *    ^          ^ ^
     * ____________________
     * |1|2|3|4|5|9|10|6|7|==>反轉兩部分
     *    ^          ^ ^
     *完成一次排序,
     * lowIndex=lowIndex+i+i-1;繼續下一次排序
     *
     * @param src
     * @param lo
     * @param mid
     * @param hi
     */
    private void merge(int[] src, int lo, int mid, int hi) {
        int lowIndex = lo;
        int highIndex = mid + 1;
        //當 lowIndex 與highIndex 重疊時.或者highIndex超出了最外層結束
        while (lowIndex < highIndex && highIndex <= hi) {
            while (lowIndex < highIndex && src[lowIndex] <= src[highIndex]) {
                lowIndex++;
            }
            int midIndex = highIndex;
            //這里的邊界值很關鍵
            while (highIndex <= hi && src[highIndex] < src[lowIndex]) {
                highIndex++;
            }
            Convert(src, lowIndex, midIndex - 1, highIndex - 1);
            //完成一次排序
            lowIndex += highIndex - midIndex;
        }
    }
    void Convert(int a[], int low, int M, int high) {
        //反轉 low...middle
        reverse(a, low, M);
        //反轉middle+1 high
        reverse(a, M + 1, high);
        //反轉low...high
        reverse(a, low, high);
    }
    private void reverse(int[] a, int low, int hi) {
        while (low < hi) {
            swap(a, low, hi);
            low++;
            hi--;
        }
    }
    private void swap(int[] a, int left, int right) {
        a[left] ^= a[right];
        a[right] ^= a[left];
        a[left] ^= a[right];
    }
}

2.2.5 排序算法的復雜度

研究復雜度的第一步是建立模型.一般來說尋找與問題相關的最簡單的模型.排序算法,是基于主鍵的比較決定的.
命題I:
沒有任何基于比較的算法能夠保證使用少于lg(N!)~NlogN次比較將長度為N的數組排序.

這個結論告訴我們在設計排序時能達到的最佳效果.
命題J:
歸并排序是一種漸進最優的基于比較的排序算法.更準確的說.歸并排序在最壞情況下的比較次數和其他基于比較的任意算法的最少時間都是~NlogN.

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • 搞懂基本排序算法 上篇文章寫了關于 Java 內部類的基本知識,感興趣的朋友可以去看一下:搞懂 JAVA 內部類;...
    醒著的碼者閱讀 1,203評論 3 4
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,265評論 0 2
  • 1 初級排序算法 排序算法關注的主要是重新排列數組元素,其中每個元素都有一個主鍵。排序算法是將所有元素主鍵按某種方...
    深度沉迷學習閱讀 1,421評論 0 1
  • 總結一下常見的排序算法。 排序分內排序和外排序。內排序:指在排序期間數據對象全部存放在內存的排序。外排序:指在排序...
    jiangliang閱讀 1,362評論 0 1
  • 坐上歷史的車輪,思緒回到2017年頭,回想2017這一整年,我還是欣慰的,畢竟年底實現了年頭的期盼是一件值得...
    Anna小魚閱讀 335評論 0 0