序言
上一篇文章我們已經(jīng)講完了插入排序,也就是說我的On^2 的算法基本就寫完了,當(dāng)然還有別的On^2 的算法,但是我這里就不一一去介紹了,個(gè)人覺得這些基本的排序算法,掌握冒泡、選擇、插入排序等基本就夠了,今天我們來談?wù)劯呒壟判蛩惴w并排序:
自頂向下的歸并排序
今天我們會(huì)講兩個(gè)版本的歸并排序,分別是自頂向下和自底向上的,我們先來談?wù)勛皂斚蛳碌臍w并排序,先看一張圖:
根據(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];
}];
看完了上面的測試結(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é)果:
可以看到雖然還是比不上插入排序,但是很明顯已經(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];
}
從上圖可以看到,現(xiàn)在基本上近乎有序的數(shù)組也能在很短的時(shí)間內(nèi)就完成了,這得益于我們的兩個(gè)優(yōu)化,怎么樣,是不是特別有成就感?自己快去試試吧。
自底向上的歸并排序
上面我們已經(jīng)說完了,自頂向下的歸并排序,可能你會(huì)問了,歸并排序還有兩種嗎?答案是:“沒錯(cuò),就是有兩種,刺不刺激?”,不過大家不用擔(dān)心,接下來要講的歸并排序還是不難的,至少代碼量是不多,我們先來看一張圖:(也是從別人博客中摳出來的圖,有冒犯的話,我立馬刪除)
下面說一下它的基本概念吧:
自底向上的歸并排序算法的思想就是數(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ì)的注釋,還有什么不理解的,可以再去看下概念和圖片,對比著理解更容易。
參考鏈接:自頂向下歸并排序和自底向上的歸并排序