序言
上一篇文章我們已經講完了插入排序,也就是說我的On^2 的算法基本就寫完了,當然還有別的On^2 的算法,但是我這里就不一一去介紹了,個人覺得這些基本的排序算法,掌握冒泡、選擇、插入排序等基本就夠了,今天我們來談談高級排序算法歸并排序:
自頂向下的歸并排序
今天我們會講兩個版本的歸并排序,分別是自頂向下和自底向上的,我們先來談談自頂向下的歸并排序,先看一張圖:
根據上面的圖片我們就很容易理解自頂向下的歸并排序是什么概念了:
自頂向下的排序算法就是把數組元素不斷的二分,直到子數組的元素個數為一個,因為這個時候子數組必定是已有序的,然后將兩個有序的序列合并成一個新的有序的序列,兩個新的有序序列又可以合并成另一個新的有序序列,以此類推,直到合并成一個有序的數組
由于我感覺自己描述的不夠清楚,借用一個博客中的話來描述一下,圖片也是來自于參考鏈接,下面會放出參考鏈接;
好了,下面我們來看下我們的代碼實現:
自頂向下歸并排序代碼實現
#pragma mark - 自頂向下的歸并排序
#pragma mark -
- (void)mergeSort:(NSMutableArray *)array {
//對數組從0 -- 數組個數 -1中間的元素進行歸并排序
[self __MergeSort:array left:0 right:(int)array.count -1];
}
/**
遞歸使用歸并排序,對arr[left...right]的范圍進行排序
@param array 數組
@param left 左邊界
@param right 右邊界
*/
- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right {
//判斷遞歸到底的情況
if (left >= right) {
//這時只有一個元素或者是不存在的情況
return;
}
//中間位置的索引
int middle = (right + left) / 2;
//對left - middle區間的元素進行排序操作
[self __MergeSort:array left:left right:middle];
//對middle + 1 - right區間的元素進行排序操作
[self __MergeSort:array left:middle + 1 right:right];
//兩邊排序完成后進行歸并操作
[self merge:array left:left middle:middle right:right];
}
/**
對[left middle] 和 [middle + 1 right]這兩個區間歸并操作
@param array 傳入的數組
@param left 左邊界
@param middle 中間位置
@param right 右邊界
*/
- (void)merge:(NSMutableArray *)array left:(int)left middle:(int)middle right:(int)right {
//拷貝一個數組出來
NSMutableArray * copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
for ( int i = left; i <= right; i++) {
//這里要注意由于有left的偏移量 所以copyArray賦值的時候要減去left
copyArray[i - left] = array[i];
}
int i = left,j = middle +1;
//循環從left開始到right區間內給數組重新賦值 注意賦值的時候也是從left開始的不要習慣性寫成了從0開始--還有都是閉區間
for (int k = left; k <= right; k++) {
//當左邊界超過中間點時 說明左半部分數組越界了 直接取右邊部分的數組的第一個元素即可
if (i > middle) {
//給數組賦值 注意偏移量left 因為這里是從left開始的
array[k] = copyArray[j - left];
//索引++
j++;
}else if (j > right) {//當j大于右邊的邊界時證明有半部分數組越界了,直接取左半部分的第一個元素即可
array[k] = copyArray[i - left];
//索引++
i++;
}else if (copyArray[i - left] > copyArray[j - left]) {//左右兩半部分數組比較
//當右半部分數組的第一個元素要小時 給數組賦值為右半部分的第一個元素
array[k] = copyArray[j - left];
//右半部分索引加1
j++;
}else {//右半部分數組首元素大于左半部分數組首元素
array[k] = copyArray[i - left];
i++;
}
}
}
雖然代碼中有詳細的注釋,但是考慮到很多沒有任何基礎的朋友,我這里簡單介紹一下:
1.在- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right ;這個方法中我們通過遞歸調用的方式,將整個數組不斷的拆分,最終當left >= right也就是拆分到只有一個元素的時候,整個拆分完的部分都是有序的,然后我們在利用- (void)merge:(NSMutableArray *)array left:(int)left middle:(int)middle right:(int)right ;方法通過將兩個有序的子數組不斷的合并,最終當所有元素都合并完成之后,數組也就有序了;
2.在我們的merge操作也就是合并操作的方法中,我們有一個關鍵的操作,也就是拷貝一個數組,元素和傳入部分的數組元素是一樣的,這里也就是整個歸并排序的核心,通過比較兩個子數組的首元素大小,將小的那個賦值給傳入的數組對應的位置,當所有的元素都考察完了,這兩個數組也就合并稱了一個有序的新數組了。
3.注意在歸并操作時 有一個left的便宜倆,也就是復制一個新數組的時候,給新數組賦值的時候注意要減去left的偏移量,在比較的時候同樣也要注意減去left的偏移量。
自頂向下歸并排序的優化
上面我們已經實現了一個歸并排序了,那么我們現在來測試一下:
//生成普通的隨機數組
NSMutableArray *normalArray = [self.testHelper generateRandomArray:50000 rangeLeft:1 rangeRight:50000];
//生成近乎有序的排序數組
NSMutableArray *nearArray = [self.testHelper generateNearlyOrderedArray:50000 swapTimes:10];
//普通的隨機數組
NSMutableArray *mergeSortNormal = normalArray.mutableCopy;
//近乎有序的數組
NSMutableArray *mergeSortNear = nearArray.mutableCopy;
//插入排序普通數組
NSMutableArray * insertionNormal = normalArray.mutableCopy;
//近乎有序的插入排序數組
NSMutableArray *insertionNear = nearArray.mutableCopy;
NSLog(@"============普通數組插入排序耗時============");
[self.testHelper testSortWithExcuteBlock:^{
[self insertionSort2:insertionNormal];
}];
NSLog(@"============普通數組歸并排序耗時============");
[self.testHelper testSortWithExcuteBlock:^{
[self mergeSort:mergeSortNormal];
}];
NSLog(@"============近乎有序插入排序耗時============");
[self.testHelper testSortWithExcuteBlock:^{
[self insertionSort2:insertionNear];
}];
NSLog(@"============近乎有序的歸并排序耗時============");
[self.testHelper testSortWithExcuteBlock:^{
[self selectionSort:mergeSortNear];
}];
看完了上面的測試結果,不知道大家是否發現了問題,我們測試發現,對于普通的數組來說歸并排序效率比插入排序快很多,但是對于近乎有序的數組來說,插入排序卻比歸并排序要好的,那么我們的第一個優化就出來了:
1.我們在- (void)__MergeSort:(NSMutableArray *)array left:(int)left right:(int)right ;中遞歸結束的條件是這樣的:
//判斷遞歸到底的情況
if (left >= right) {
//這時只有一個元素或者是不存在的情況
return;
}
當元素比較小的時候,這里我們可以通過使用插入排序來進行排序達到優化:
//歸并排序的第1個優化 當是小規模數組的時候使用插入排序
if (right - left <= 15) {//小數值范圍的排序使用插入排序,因為會有大量重復的元素 ,而插入排序在重復元素的排序上 效率是相當高的
[self insertionSort3:array left:left right:right];
return;
}
我們看一下優化后的結果:
可以看到雖然還是比不上插入排序,但是很明顯已經有一些優化了,下面我們看下一個優化點:
2.我們在merge操作時,直接進行了下面的歸并操作:
//兩邊排序完成后進行歸并操作
[self merge:array left:left middle:middle right:right];
我們來分析一下到底有沒有必要每次都進行歸并操作呢?其實很明顯是沒有必要的,為什么這么說呢,我們假如要對下面兩個數組進行歸并:
NSArray *array1 = @[@1,@2,@3,@4];
NSArray *array2 = @[@6,@8,@10,@14];
很明顯array1的最后一個元素已經小于array2的第一個元素,那么完全是沒有比較進行歸并操作的,所以,第二個優化的就這么寫:
//優化點2 只有當左半部分的最后一個元素大于右邊部分的第一個元素時才歸并 否則不歸并 因為本來就已經有序了
if (array[middle] > array[middle + 1]) {
//兩邊排序完成后進行歸并操作
[self merge:array left:left middle:middle right:right];
}
從上圖可以看到,現在基本上近乎有序的數組也能在很短的時間內就完成了,這得益于我們的兩個優化,怎么樣,是不是特別有成就感?自己快去試試吧。
自底向上的歸并排序
上面我們已經說完了,自頂向下的歸并排序,可能你會問了,歸并排序還有兩種嗎?答案是:“沒錯,就是有兩種,刺不刺激?”,不過大家不用擔心,接下來要講的歸并排序還是不難的,至少代碼量是不多,我們先來看一張圖:(也是從別人博客中摳出來的圖,有冒犯的話,我立馬刪除)
下面說一下它的基本概念吧:
自底向上的歸并排序算法的思想就是數組中先一個一個歸并成兩兩有序的序列,兩兩有序的序列歸并成四個四個有序的序列,然后四個四個有序的序列歸并八個八個有序的序列,以此類推,直到,歸并的長度大于整個數組的長度,此時整個數組有序。需要注意的是數組按照歸并長度劃分,最后一個子數組可能不滿足長度要求,這個情況需要特殊處理。自頂下下的歸并排序算法一般用遞歸來實現,而自底向上可以用循環來實現。
下面我們送上代碼實現:
#pragma mark - 自底向上的歸并排序
#pragma mark -
- (void)mergeBU:(NSMutableArray *)array {
//size是每一次歸并的大小 如第一次size為2時就兩個元素一組歸并 然后依次為size的倍數 4個元素一組 8個元素一組進行歸并 直到等于array.count就結束
for (int size = 1; size < array.count; size += size) {
//i表示的是歸并時開始的位置,如i= 0 的時候就歸并第0個和第一個元素,依次疊加size的兩倍,比如size為1且i = 0過后 i就變成了2也即是歸并第2個元素和第三個元素,依次又是歸并第4個和第五個元素,依次類推,
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) ];
}
}
}
好了今天就講到這里了,所有的代碼我都會有詳細的注釋,還有什么不理解的,可以再去看下概念和圖片,對比著理解更容易。
參考鏈接:自頂向下歸并排序和自底向上的歸并排序