使用OC寫算法之歸并排序

序言

上一篇文章我們已經(jīng)講完了插入排序,也就是說我的On^2 的算法基本就寫完了,當(dāng)然還有別的On^2 的算法,但是我這里就不一一去介紹了,個(gè)人覺得這些基本的排序算法,掌握冒泡、選擇、插入排序等基本就夠了,今天我們來談?wù)劯呒壟判蛩惴w并排序:

自頂向下的歸并排序

今天我們會(huì)講兩個(gè)版本的歸并排序,分別是自頂向下和自底向上的,我們先來談?wù)勛皂斚蛳碌臍w并排序,先看一張圖:

自頂向下的歸并排序.png

根據(jù)上面的圖片我們就很容易理解自頂向下的歸并排序是什么概念了:

自頂向下的排序算法就是把數(shù)組元素不斷的二分,直到子數(shù)組的元素個(gè)數(shù)為一個(gè),因?yàn)檫@個(gè)時(shí)候子數(shù)組必定是已有序的,然后將兩個(gè)有序的序列合并成一個(gè)新的有序的序列,兩個(gè)新的有序序列又可以合并成另一個(gè)新的有序序列,以此類推,直到合并成一個(gè)有序的數(shù)組

由于我感覺自己描述的不夠清楚,借用一個(gè)博客中的話來描述一下,圖片也是來自于參考鏈接,下面會(huì)放出參考鏈接;
好了,下面我們來看下我們的代碼實(shí)現(xiàn):

自頂向下歸并排序代碼實(shí)現(xiàn)
#pragma  mark -  自頂向下的歸并排序
#pragma  mark -
- (void)mergeSort:(NSMutableArray *)array {
    //對數(shù)組從0 -- 數(shù)組個(gè)數(shù) -1中間的元素進(jìn)行歸并排序
    [self __MergeSort:array left:0 right:(int)array.count -1];
}


/**
 遞歸使用歸并排序,對arr[left...right]的范圍進(jìn)行排序
 @param array 數(shù)組
 @param left 左邊界
 @param right 右邊界
 */
- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right {
    //判斷遞歸到底的情況
    if (left >= right) {
         //這時(shí)只有一個(gè)元素或者是不存在的情況
          return;
    }
    //中間位置的索引
    int middle = (right + left) / 2;
    //對left  -  middle區(qū)間的元素進(jìn)行排序操作
    [self __MergeSort:array left:left right:middle];
    //對middle + 1  -  right區(qū)間的元素進(jìn)行排序操作
    [self __MergeSort:array left:middle + 1 right:right];
    //兩邊排序完成后進(jìn)行歸并操作
    [self merge:array left:left middle:middle right:right];
}


/**
 對[left middle] 和 [middle + 1 right]這兩個(gè)區(qū)間歸并操作

 @param array 傳入的數(shù)組
 @param left 左邊界
 @param middle 中間位置
 @param right 右邊界
 */
- (void)merge:(NSMutableArray *)array left:(int)left middle:(int)middle right:(int)right {
    //拷貝一個(gè)數(shù)組出來
    NSMutableArray * copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
    
    for ( int i = left; i <= right; i++) {
        //這里要注意由于有l(wèi)eft的偏移量 所以copyArray賦值的時(shí)候要減去left
        copyArray[i - left] = array[i];
    }
    
    int i = left,j = middle +1;
    //循環(huán)從left開始到right區(qū)間內(nèi)給數(shù)組重新賦值 注意賦值的時(shí)候也是從left開始的不要習(xí)慣性寫成了從0開始--還有都是閉區(qū)間
    for (int k = left; k <= right; k++) {
        //當(dāng)左邊界超過中間點(diǎn)時(shí) 說明左半部分?jǐn)?shù)組越界了 直接取右邊部分的數(shù)組的第一個(gè)元素即可
        if (i > middle) {
            //給數(shù)組賦值 注意偏移量left 因?yàn)檫@里是從left開始的
            array[k] = copyArray[j - left];
            //索引++
            j++;
        }else if (j > right) {//當(dāng)j大于右邊的邊界時(shí)證明有半部分?jǐn)?shù)組越界了,直接取左半部分的第一個(gè)元素即可
            array[k] = copyArray[i - left];
            //索引++
            i++;
        }else if (copyArray[i - left] > copyArray[j - left]) {//左右兩半部分?jǐn)?shù)組比較
            //當(dāng)右半部分?jǐn)?shù)組的第一個(gè)元素要小時(shí) 給數(shù)組賦值為右半部分的第一個(gè)元素
            array[k] = copyArray[j - left];
            //右半部分索引加1
            j++;
        }else {//右半部分?jǐn)?shù)組首元素大于左半部分?jǐn)?shù)組首元素
            array[k] = copyArray[i - left];
            i++;
        }
    }
}

雖然代碼中有詳細(xì)的注釋,但是考慮到很多沒有任何基礎(chǔ)的朋友,我這里簡單介紹一下:
1.在- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right ;這個(gè)方法中我們通過遞歸調(diào)用的方式,將整個(gè)數(shù)組不斷的拆分,最終當(dāng)left >= right也就是拆分到只有一個(gè)元素的時(shí)候,整個(gè)拆分完的部分都是有序的,然后我們在利用- (void)merge:(NSMutableArray *)array left:(int)left middle:(int)middle right:(int)right ;方法通過將兩個(gè)有序的子數(shù)組不斷的合并,最終當(dāng)所有元素都合并完成之后,數(shù)組也就有序了;
2.在我們的merge操作也就是合并操作的方法中,我們有一個(gè)關(guān)鍵的操作,也就是拷貝一個(gè)數(shù)組,元素和傳入部分的數(shù)組元素是一樣的,這里也就是整個(gè)歸并排序的核心,通過比較兩個(gè)子數(shù)組的首元素大小,將小的那個(gè)賦值給傳入的數(shù)組對應(yīng)的位置,當(dāng)所有的元素都考察完了,這兩個(gè)數(shù)組也就合并稱了一個(gè)有序的新數(shù)組了。
3.注意在歸并操作時(shí) 有一個(gè)left的便宜倆,也就是復(fù)制一個(gè)新數(shù)組的時(shí)候,給新數(shù)組賦值的時(shí)候注意要減去left的偏移量,在比較的時(shí)候同樣也要注意減去left的偏移量。

自頂向下歸并排序的優(yōu)化

上面我們已經(jīng)實(shí)現(xiàn)了一個(gè)歸并排序了,那么我們現(xiàn)在來測試一下:

    //生成普通的隨機(jī)數(shù)組
    NSMutableArray *normalArray = [self.testHelper generateRandomArray:50000 rangeLeft:1 rangeRight:50000];
    //生成近乎有序的排序數(shù)組
    NSMutableArray *nearArray = [self.testHelper generateNearlyOrderedArray:50000 swapTimes:10];
    //普通的隨機(jī)數(shù)組
    NSMutableArray *mergeSortNormal = normalArray.mutableCopy;
    //近乎有序的數(shù)組
    NSMutableArray *mergeSortNear = nearArray.mutableCopy;
    //插入排序普通數(shù)組
    NSMutableArray * insertionNormal = normalArray.mutableCopy;
    //近乎有序的插入排序數(shù)組
    NSMutableArray *insertionNear = nearArray.mutableCopy;

    NSLog(@"============普通數(shù)組插入排序耗時(shí)============");
    [self.testHelper testSortWithExcuteBlock:^{
        [self insertionSort2:insertionNormal];
    }];
    
    NSLog(@"============普通數(shù)組歸并排序耗時(shí)============");
    [self.testHelper testSortWithExcuteBlock:^{
        [self mergeSort:mergeSortNormal];
    }];
    
    NSLog(@"============近乎有序插入排序耗時(shí)============");
    [self.testHelper testSortWithExcuteBlock:^{
        [self insertionSort2:insertionNear];
    }];
    
    NSLog(@"============近乎有序的歸并排序耗時(shí)============");
    [self.testHelper testSortWithExcuteBlock:^{
        [self selectionSort:mergeSortNear];
    }];

插入排序有歸并排序耗時(shí)對比.png

看完了上面的測試結(jié)果,不知道大家是否發(fā)現(xiàn)了問題,我們測試發(fā)現(xiàn),對于普通的數(shù)組來說歸并排序效率比插入排序快很多,但是對于近乎有序的數(shù)組來說,插入排序卻比歸并排序要好的,那么我們的第一個(gè)優(yōu)化就出來了:
1.我們在- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right ;中遞歸結(jié)束的條件是這樣的:

    //判斷遞歸到底的情況
    if (left >= right) {
         //這時(shí)只有一個(gè)元素或者是不存在的情況
          return;
    }

當(dāng)元素比較小的時(shí)候,這里我們可以通過使用插入排序來進(jìn)行排序達(dá)到優(yōu)化:

    //歸并排序的第1個(gè)優(yōu)化 當(dāng)是小規(guī)模數(shù)組的時(shí)候使用插入排序
    if (right - left <= 15) {//小數(shù)值范圍的排序使用插入排序,因?yàn)闀?huì)有大量重復(fù)的元素 ,而插入排序在重復(fù)元素的排序上 效率是相當(dāng)高的
        [self insertionSort3:array left:left right:right];
        return;
    }
  

我們看一下優(yōu)化后的結(jié)果:

第一個(gè)優(yōu)化后歸并排序與插入排序?qū)Ρ?png

可以看到雖然還是比不上插入排序,但是很明顯已經(jīng)有一些優(yōu)化了,下面我們看下一個(gè)優(yōu)化點(diǎn):

2.我們在merge操作時(shí),直接進(jìn)行了下面的歸并操作:

     //兩邊排序完成后進(jìn)行歸并操作
    [self merge:array left:left middle:middle right:right];

我們來分析一下到底有沒有必要每次都進(jìn)行歸并操作呢?其實(shí)很明顯是沒有必要的,為什么這么說呢,我們假如要對下面兩個(gè)數(shù)組進(jìn)行歸并:

NSArray *array1 = @[@1,@2,@3,@4];
NSArray *array2 = @[@6,@8,@10,@14];

很明顯array1的最后一個(gè)元素已經(jīng)小于array2的第一個(gè)元素,那么完全是沒有比較進(jìn)行歸并操作的,所以,第二個(gè)優(yōu)化的就這么寫:

  //優(yōu)化點(diǎn)2 只有當(dāng)左半部分的最后一個(gè)元素大于右邊部分的第一個(gè)元素時(shí)才歸并 否則不歸并 因?yàn)楸緛砭鸵呀?jīng)有序了
    if (array[middle] > array[middle + 1]) {
        //兩邊排序完成后進(jìn)行歸并操作
        [self merge:array left:left middle:middle right:right];
    }
第二個(gè)優(yōu)化后對比.png

從上圖可以看到,現(xiàn)在基本上近乎有序的數(shù)組也能在很短的時(shí)間內(nèi)就完成了,這得益于我們的兩個(gè)優(yōu)化,怎么樣,是不是特別有成就感?自己快去試試吧。

自底向上的歸并排序

上面我們已經(jīng)說完了,自頂向下的歸并排序,可能你會(huì)問了,歸并排序還有兩種嗎?答案是:“沒錯(cuò),就是有兩種,刺不刺激?”,不過大家不用擔(dān)心,接下來要講的歸并排序還是不難的,至少代碼量是不多,我們先來看一張圖:(也是從別人博客中摳出來的圖,有冒犯的話,我立馬刪除)

image.png

下面說一下它的基本概念吧:

自底向上的歸并排序算法的思想就是數(shù)組中先一個(gè)一個(gè)歸并成兩兩有序的序列,兩兩有序的序列歸并成四個(gè)四個(gè)有序的序列,然后四個(gè)四個(gè)有序的序列歸并八個(gè)八個(gè)有序的序列,以此類推,直到,歸并的長度大于整個(gè)數(shù)組的長度,此時(shí)整個(gè)數(shù)組有序。需要注意的是數(shù)組按照歸并長度劃分,最后一個(gè)子數(shù)組可能不滿足長度要求,這個(gè)情況需要特殊處理。自頂下下的歸并排序算法一般用遞歸來實(shí)現(xiàn),而自底向上可以用循環(huán)來實(shí)現(xiàn)。
下面我們送上代碼實(shí)現(xiàn):

#pragma  mark -  自底向上的歸并排序
#pragma  mark -
- (void)mergeBU:(NSMutableArray *)array {
    //size是每一次歸并的大小 如第一次size為2時(shí)就兩個(gè)元素一組歸并 然后依次為size的倍數(shù) 4個(gè)元素一組 8個(gè)元素一組進(jìn)行歸并 直到等于array.count就結(jié)束
    for (int size = 1; size < array.count; size +=  size) {
        //i表示的是歸并時(shí)開始的位置,如i= 0 的時(shí)候就歸并第0個(gè)和第一個(gè)元素,依次疊加size的兩倍,比如size為1且i = 0過后 i就變成了2也即是歸并第2個(gè)元素和第三個(gè)元素,依次又是歸并第4個(gè)和第五個(gè)元素,依次類推,
        for (int i = 0; i + size< array.count; i += size + size) {
            int middle = i + size - 1;
            [self merge:array left:i middle:middle right:(int)MIN(i+size+size - 1, array.count - 1) ];
        }
    }
}

好了今天就講到這里了,所有的代碼我都會(huì)有詳細(xì)的注釋,還有什么不理解的,可以再去看下概念和圖片,對比著理解更容易。
參考鏈接:自頂向下歸并排序和自底向上的歸并排序

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一. 寫在前面 要學(xué)習(xí)算法,“排序”是一個(gè)回避不了的重要話題,在分析完并查集算法和常用數(shù)據(jù)結(jié)構(gòu)之后,今天我們終于可...
    Leesper閱讀 2,555評論 0 40
  • Ba la la la ~ 讀者朋友們,你們好啊,又到了冷鋒時(shí)間,話不多說,發(fā)車! 1.冒泡排序(Bub...
    王飽飽閱讀 1,816評論 0 7
  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,223評論 0 52
  • 來到杭州五個(gè)月了。 這五個(gè)月都沒有去找工作,住在表哥這邊,玩了五個(gè)月的電腦。 說實(shí)話,五個(gè)月之前還偶爾抽出一點(diǎn)空來...
    赤壁傷痕閱讀 233評論 0 0
  • 再次見她跟印象里的很是不同,微紅的頭發(fā)貼著兩鬢精心梳好,用一根銀色的簪子別在腦后,只留出兩根長長的鬢角,末端燙成c...
    白馬陳慶之閱讀 164評論 0 0