1. 程式人生 > 實用技巧 >手撕排序演算法 - iOS進階必備

手撕排序演算法 - iOS進階必備

氣泡排序

氣泡排序是通過比較兩個相鄰元素的大小實現排序,如果前一個元素大於後一個元素,就交換這兩個元素。這樣就會讓每一趟冒泡都能找到最大一個元素並放到最後。

以[ 8, 1, 4, 6, 2, 3, 5, 7 ]為例,對它進行氣泡排序:

程式碼實現:

+ (NSArray *)bubbleSort:(NSArray *)unsortDatas {
    NSMutableArray *unSortArray = [unsortDatas mutableCopy];
    for (int i = 0; i < unSortArray.count -1 ; i++) {
        BOOL isChange = NO;
        for (int j = 0; j < unSortArray.count - 1 - i; j++) {
            // 比較相鄰兩個元素的大小,後一個大於前一個就交換
            if ([unSortArray[j] integerValue] > [unSortArray[j+1] integerValue]) {
                NSNumber *data = unSortArray[j+1];
                unSortArray[j+1] = unSortArray[j];
                unSortArray[j] = data;
                isChange = YES;
            }
        }
        if (!isChange) {
            // 如果某次未發生資料交換,說明資料已排序
            break;
        }
    }
    return [unSortArray copy];
}

特點

穩定性:它是指對同樣的資料進行排序,會不會改變它的相對位置。比如[ 1, 3,2, 4,2]經過排序後,兩個相同的元素 2 位置會不會被交換。氣泡排序是比較相鄰兩個元素的大小,顯然不會破壞穩定性。

空間複雜度:由於整個排序過程是在原資料上進行操作,故為 O(1);

時間複雜度:由於嵌套了 2 層迴圈,故為 O(n*n);

選擇排序

選擇排序的思想是,依次從「無序列表」中找到一個最小的元素放到「有序列表」的最後面。以 arr = [ 8, 1, 4, 6, 2, 3, 5, 4 ]為例,排序開始時把 arr 分為有序列表 A = [ ], 無序列表 B =[ 8, 1, 4, 6, 2, 3, 5, 4 ],依次從 B 中找出最小的元素放到 A 的最後面。這種排序也是邏輯上的分組,實際上不會建立 A 和 B,只是用下標來標記 A 和 B。

以 arr = [ 8, 1, 4, 6, 2, 3, 5, 4 ]為例,第一次找到最小元素 1 與 8 進行交換,這時有列表 A = [1], 無序列表 B = [8, 4, 6, 2, 3, 5, 4];第二次從 B 中找到最小元素 2,與 B 中的第一個元素進行交換,交換後 A = [1,2],B = [4, 6, 8, 3, 5, 4];就這樣不斷縮短 B,擴大 A,最終達到有序。

作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這有個iOS交流群:642363427,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術!

程式碼實現:

+ (NSArray *)seelectSort:(NSArray *)unsortDatas {
    NSMutableArray *unSortArray = [unsortDatas mutableCopy];
    for (int i = 0; i < unSortArray.count; i++) {
        int mindex = i;
        for (int j = i; j < unSortArray.count; j++) {
            // 找到最小元素的index
            if ([unSortArray[j] integerValue] < [unSortArray[mindex] integerValue]) {
                mindex = j;
            }
        }
        // 交換位置
        NSNumber *data = unSortArray[i];
        unSortArray[i] = unSortArray[mindex];
        unSortArray[mindex] = data;
    }
    return [unSortArray copy];
}

特點

穩定性:排序過程中元素是按順序進行遍歷,相同元素相對位置不會發生變化,故穩定。

空間複雜度:在原序列進行操作,故為 O( 1 );

時間複雜度:需要 2 次迴圈遍歷,故為 O( n * n );

插入排序

在整個排序過程如圖所示,以 arr = [ 8, 1, 4, 6, 2, 3, 5, 7]為例,它會把 arr 分成兩組 A = [ 8 ] 和 B =[ 1, 4, 6, 2, 3, 5, 7],逐步遍歷 B 中元素插入到 A 中,最終構成一個有序序列:

程式碼實現:

+ (NSArray *)insertionSort:(NSArray *)unsortDatas {
    NSMutableArray *unSortArray = [unsortDatas mutableCopy];
    int preindx = 0;
    NSNumber *current;
    for (int i = 1; i < unSortArray.count; i++) {
        preindx = i - 1;
        // 必須記錄這個元素,不然會被覆蓋掉
        current = unSortArray[i];
        // 逆序遍歷已經排序好的陣列

        // 當前元素小於排序好的元素,就移動到下一個位置
        while (preindx >= 0 && [current integerValue] < [unSortArray[preindx] integerValue] ) {
            // 元素向後移動
            unSortArray[preindx+1] = unSortArray[preindx];
            preindx -= 1;
        }
        // 找到合適的位置,把當前的元素插入
        unSortArray[preindx+1] = current;
    }
    return [unSortArray copy];
}

特點

穩定性:它是從後往前遍歷已排序好的序列,相同元素不會改變位置,故為穩定排序;
空間複雜度:它是在原序列進行排序,故為 O ( 1 );

時間複雜度:排序的過程中,首先要遍歷所有的元素,然後在已排序序列中找到合適的位置並插入。共需要 2 層迴圈,故為 O ( n * n );

希爾排序

希爾排序,它是由 D.L.Shell 於1959 年提出而得名。根據它的名字很難想象演算法的核心思想。[所以只能死記硬背了,面試官問:希爾排序的思想是什麼?]。它的核心思想是把一個序列分組,對分組後的內容進行插入排序,這裡的分組只是邏輯上的分組,不會重新開闢儲存空間。它其實是插入排序的優化版,插入排序對基本有序的序列效能好,希爾排序利用這一特性把原序列分組,對每個分組進行排序,逐步完成排序。

以 arr =[ 8, 1, 4, 6, 2, 3, 5, 7 ]為例,通過 floor(8/2)來分為 4 組,8 表示陣列中元素的個數。分完組後,對組內元素進行插入排序。

「 第1次分組 」

「 利用第1次分組結果進行第2次分組」

「 利用第2次分組結果進行最後一次分組」

程式碼實現:

+ (NSArray *)shellSort:(NSArray *)unsortDatas {
    NSMutableArray *unSortArray = [unsortDatas mutableCopy];
    // len = 9
    int len = (int)unSortArray.count;
    // floor 向下取整,所以 gap的值為:4,2,1
    for (int gap = floor(len / 2); gap > 0; gap = floor(gap/2)) {
        // i=4;i<9;i++ (4,5,6,7,8)
        for (int i = gap; i < len; i++) {
            // j=0,1,2,3,4
            // [0]-[4] [1]-[5] [2]-[6] [3]-[7] [4]-[8]
            for (int j = i - gap; j >= 0 && [unSortArray[j] integerValue] > [unSortArray[j+gap] integerValue]; j-=gap) {
                // 交換位置
                NSNumber *temp = unSortArray[j];
                unSortArray[j] = unSortArray[gap+j];
                unSortArray[gap+j] = temp;
            }
        }
    }
    return [unSortArray copy];
}

特點

穩定性:它可能會把相同元素分到不同的組中,那麼兩個相同的元素就有可能調換相對位置,故不穩定。

空間複雜度:由於整個排序過程是在原資料上進行操作,故為 O(1);

時間複雜度:希爾排序的時間複雜度與增量序列的選取有關,例如希爾增量時間複雜度為O(n²),而Hibbard增量的希爾排序的時間複雜度為O(logn的3/2),希爾排序時間複雜度的下界是n*log2n

快速排序

快速排序的核心思想是對待排序序列通過一個「支點」(支點就是序列中的一個元素,別把它想的太高大上)進行拆分,使得左邊的資料小於支點,右邊的資料大於支點。然後把左邊和右邊再做一次遞迴,直到遞迴結束。支點的選擇也是一門大學問,我們以(左邊index +右邊index)/ 2 來選擇支點。

以 arr = [ 8, 1, 4, 6, 2, 3, 5, 7 ]為例,選擇一個支點, index= (L+R)/2 = (0+7)/2=3, 支點的值 pivot = arr[index]= arr[3]= 6,接下來需要把 arr 中小於 6 的移到左邊,大於 6 的移到右邊。

快速排序使用一個高效的方法做資料拆分。

用一個指向左邊的遊標 i,和指向右邊的遊標 j,逐漸移動這兩個遊標,直到找到 arr[i]> 6 和 arr[j]< 6, 停止移動遊標,交換 arr[i]和 arr[j],交換完後 i++,j--(對下一個元素進行比較),直到 i>=j,停止移動。

圖中的 L,R 是指快速排序開始時序列的起始和結束索引,在一趟快速排序中,它們的值不會發生改變,直到下一趟排序時才會改變。

一趟快速排序完成後,分別對小於6和大於等於6的部分進行快速排序,遞迴就好了。對[ 5, 1, 4, 3, 2 ]進行一趟快速排序。

程式碼實現:

/**
 快速排序

 @param unSortArray 待排序序列
 @param lindex 待排序序列左邊的index
 @param rIndex 待排序序列右邊的index
 @return 排序結果
 */
+ (NSArray *)quickSort:(NSMutableArray *)unSortArray leftIndex:(NSInteger)lindex rightIndex:(NSInteger)rIndex {
    NSInteger i = lindex; NSInteger j = rIndex;
    // 取中間的值作為一個支點
    NSNumber *pivot = unSortArray[(lindex + rIndex) / 2];
    while (i <= j) {
        // 向左移動,直到找打大於支點的元素
        while ([unSortArray[i] integerValue] < [pivot integerValue]) {
            i++;
        }
        // 向右移動,直到找到小於支點的元素
        while ([unSortArray[j] integerValue] > [pivot integerValue]) {
            j--;
        }
        // 交換兩個元素,讓左邊的大於支點,右邊的小於支點
        if (i <= j) {
            // 如果 i== j,交換個啥?
            if (i != j) {
                NSNumber *temp = unSortArray[i];
                unSortArray[i] = unSortArray[j];
                unSortArray[j] = temp; }
            i++;
            j--;
        }
    }
    // 遞迴左邊,進行快速排序
    if (lindex < j) {
        [self quickSort:unSortArray leftIndex:lindex rightIndex:j];
    }
    // 遞迴右邊,進行快速排序
    if (i < rIndex) {
        [self quickSort:unSortArray leftIndex:i rightIndex:rIndex];
    }
    return [unSortArray copy];
}

歸併排序

歸併排序,採用分治思想,先把待排序序列拆分成一個個子序列,直到子序列只有一個元素,停止拆分,然後對每個子序列進行邊排序邊合併。其實,從名字「歸併」可以看出一絲「拆、合」的意思(妄加猜測)。

以 arr = [ 8, 1, 4, 6, 2, 3, 5, 7 ]為例,排序需要分兩步:

a、「」,以 length/2 拆分為 A = [ 8, 1, 4, 6 ] ,B = [ 2, 3, 5, 7 ],繼續對 A 和 B 進行拆分,A1 = [ 8, 1 ] 、A2 = [ 4, 6 ]、B1 = [ 2, 3 ]、B2 = [ 5, 7 ],繼續拆分,直到只有一個元素,A11 = [ 8 ] , A12=[ 1 ] 、A21 = [ 4 ]、A22 = [ 6 ]、B11 = [ 2 ]、B12 = [ 3 ]、B21 = [ 5 ]、B22 = [ 7 ]。

b、「」,對單個元素的序列進行合併,A11和A12合併為[ 1, 8 ], A21 和 A22 合併為 [ 4, 6 ],等等。在合併的過程中也需要排序。

程式碼實現:

+ (NSArray *)mergeSort:(NSArray *)unSortArray {
    NSInteger len = unSortArray.count;
    // 遞迴終止條件
    if (len <= 1) {
        return unSortArray;
    }
    NSInteger mid = len / 2;
    // 對左半部分進行拆分
    NSArray *lList = [self mergeSort:[unSortArray subarrayWithRange:NSMakeRange(0, mid)]];
    // 對右半部分進行拆分
    NSArray *rList = [self mergeSort:[unSortArray subarrayWithRange:NSMakeRange(mid, len-mid)]];
    // 遞迴結束後執行下面的語句
    NSInteger lIndex = 0;
    NSInteger rIndex = 0;
    // 進行合併
    NSMutableArray *results = [NSMutableArray array];
    while (lIndex < lList.count && rIndex < rList.count) {
        if ([lList[lIndex] integerValue] < [rList[rIndex] integerValue]) {
            [results addObject:lList[lIndex]];
            lIndex += 1;
        } else {
            [results addObject:rList[rIndex]];
            rIndex += 1;
        }
    }
    // 把左邊剩餘元素加到排序結果中
    if (lIndex < lList.count) {
        [results addObjectsFromArray:[lList subarrayWithRange:NSMakeRange(lIndex, lList.count-lIndex)]];
    }
    // 把右邊剩餘元素加到排序結果中
    if (rIndex < rList.count) {
        [results addObjectsFromArray:[rList subarrayWithRange:NSMakeRange(rIndex, rList.count-rIndex)]];
    }
    return results;
}

特點

穩定性:在元素拆分的時候,雖然相同元素可能被分到不同的組中,但是合併的時候相同元素相對位置不會發生變化,故穩定。

空間複雜度:需要用到一個數組儲存排序結果,也就是合併的時候,需要開闢空間來儲存排序結果,故為 O ( n );

時間複雜度:最好最壞都為 O(nlogn);

計數排序

前面所講的 6 種排序都是基於「比較」的思想,總是在比較兩個元素的大小,然後交換位置。

現在來換個“口味”,來看看計數排序。

計數排序的核心思想是把一個無序序列 A 轉換成另一個有序序列 B,從 B 中逐個“取出”所有元素,取出的元素即為有序序列「沒看明白,不急,後面來張圖就搞明白了」。這種演算法比快速排序還要快「特定條件下」,它適用於待排序序列中元素的取值範圍比較小。比如對某大型公司員工按年齡排序,年齡的取值範圍很小,大約在(10-100)之間。

對陣列 arr = [ 8, 1, 4, 6, 2, 3, 5, 4 ] 進行排序,使用計數排序需要找到與其對應的一個有序序列,可以使用陣列的下標與 arr 做一個對映「陣列的下標恰好是有序的」。

遍歷 arr,把 arr 中的元素放到 counArr 中,counArr 的大小是由 arr 中最大元素和最小元素決定的。

圖中有個技巧,為了讓 countArr 儘可能地小,countArr 的長度使用了 arr 中的最大值 max - arr 中的最小值 min + 1 (max - min + 1),arr[i] - min 恰好是 countArr 的下標。countArr 中記錄了某個值出現的次數,比如 8 出現過 1 次,則在 countArr 中的值為 1;4 出現過 2 次,則在 countArr 中的值為 2。

程式碼實現:

+ (NSArray *)countingSort:(NSArray *)datas {
    // 1.找出陣列中最大數和最小數
    NSNumber *max = [datas firstObject];
    NSNumber *min = [datas firstObject];
    for (int i = 0; i < datas.count; i++) {
        NSNumber *item = datas[i];
        if ([item integerValue] > [max integerValue]) {
            max = item;
        }
        if ([item integerValue] < [min integerValue]) {
            min = item;
        }
    }
    // 2.建立一個數組 countArr 來儲存 datas 中元素出現的個數
    NSInteger sub = [max integerValue] - [min integerValue] + 1;
    NSMutableArray *countArr = [NSMutableArray arrayWithCapacity:sub];
    for (int i = 0; i < sub; i++) {
        [countArr addObject:@(0)];
    }
    // 3.把 datas 轉換成 countArr,使用 datas[i] 與 countArr 的下標對應起來
    for (int i = 0; i < datas.count; i++) {
        NSNumber *aData = datas[i];
        NSInteger index = [aData integerValue] - [min integerValue];
        countArr[index] = @([countArr[index] integerValue] + 1);
    }
    // 4.從countArr中輸出結果
    NSMutableArray *resultArr = [NSMutableArray arrayWithCapacity:datas.count];
    for (int i = 0; i < countArr.count; i++) {
        NSInteger count = [countArr[i] integerValue];
        while (count > 0) {
            [resultArr addObject:@(i + [min integerValue])];
            count -= 1;
        }
    }
    return [resultArr copy];
}

特點

穩定性:在元素往 countArr 中記錄時按順序遍歷,從 countArr 中取出元素也是按順序取出,相同元素相對位置不會發生變化,故穩定。

空間複雜度:需要額外申請空間,複雜度為“桶”的個數,故為 O ( k ), k 為“桶”的個數,也就是 countArr 的長度;

時間複雜度:最好最壞都為 O(n+k), k 為“桶”的個數,也就是 countArr 的長度;

以 arr = [ 8, 1, 4, 6, 2, 3, 5, 7 ]為例,排序前需要確定桶的個數,和確定桶中元素的取值範圍:

程式碼實現:

+ (NSArray *)bucketSort:(NSArray *)datas {
    // 1.找出陣列中最大數和最小數
    NSNumber *max = [datas firstObject];
    NSNumber *min = [datas firstObject];
    for (int i = 0; i < datas.count; i++) {
        NSNumber *item = datas[i];
        if ([item integerValue] > [max integerValue]) {
            max = item;
        }
        if ([item integerValue] < [min integerValue]) {
            min = item;
        }
    }
    // 2.建立桶,桶的個數為 3
    int maxBucket = 3;
    NSMutableArray *buckets = [NSMutableArray arrayWithCapacity:maxBucket];
    for (int i = 0; i < maxBucket; i++) {
        NSMutableArray *aBucket = [NSMutableArray array];
        [buckets addObject:aBucket];
    }
    // 3.把資料分配到桶中,桶中的資料是有序的
    // a.計算桶中資料的平均值,這樣分組資料的時候會把資料放到對應的桶中
    float space = ([max integerValue] - [min integerValue] + 1) / (maxBucket*1.0);
    for (int i = 0; i < datas.count; i++) {
        // b.根據資料值計算它在桶中的位置
        int index = floor(([datas[i] integerValue] - [min integerValue]) / space);
        NSMutableArray *bucket = buckets[index];
        int maxCount = (int)bucket.count;
        NSInteger minIndex = 0;
        for (int j = maxCount - 1; j >= 0; j--) {
            if ([datas[i] integerValue] > [bucket[j] integerValue]) {
                minIndex = j+1;
                break;
            }
        }
        [bucket insertObject:datas[i] atIndex:minIndex];
    }
    // 4.把桶中的資料重新組裝起來
    NSMutableArray *results = [NSMutableArray array];
    [buckets enumerateObjectsUsingBlock:^(NSArray *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [results addObjectsFromArray:obj];
    }];

    return results;
}

特點

穩定性:在元素拆分的時候,相同元素會被分到同一組中,合併的時候也是按順序合併,故穩定。

空間複雜度:桶的個數加元素的個數,為 O ( n + k );

時間複雜度:最好為 O( n + k ),最壞為 O(n * n);

基數排序

基數排序是從待排序序列找出可以作為排序的「關鍵字」,按照「關鍵字」進行多次排序,最終得到有序序列。比如對 100 以內的序列 arr = [ 3, 9, 489, 1, 5, 10, 2, 7, 6, 204 ]進行排序,排序關鍵字為「個位數」、「十位數」和「百位數」這 3 個關鍵字,分別對這 3 個關鍵字進行排序,最終得到一個有序序列。

以 arr = [ 3, 9, 489, 1, 5, 10, 2, 7, 6, 204 ]為例,最大為 3 位數,分別對個、十、百位進行排序,最終得到的序列就是有序序列。可以把 arr 看成[ 003, 009, 489, 001, 005, 010, 002, 007, 006, 204 ],這樣理解起來比較簡單。

數字的取值範圍為 0-9,故可以分為 10 個桶。

程式碼實現:

+ (NSArray *)radixSort:(NSArray *)datas {
    NSMutableArray *tempDatas;
    NSInteger maxValue = 0;
    int maxDigit = 0;
    int level = 0;
    do {
        // 1.建立10個桶
        NSMutableArray *buckets = [NSMutableArray array];
        for (int i = 0; i < 10; i++) {
            NSMutableArray *array = [NSMutableArray array];
            [buckets addObject:array];
        }
        // 2.把數儲存到桶中
        for (int i = 0; i < datas.count; i++) {
            NSInteger value = [datas[i] integerValue];
            // 求一個數的多次方
            int xx = (level < 1 ? 1 : (pow(10, level)));
            // 求個位數、十位數....
            int mod = value / xx  % 10;
            [buckets[mod] addObject:datas[i]];
            // 求最大數為了計算最大數
            if (maxDigit == 0) {
                if (value > maxValue) {
                    maxValue = value;
                }
            }
        }
        // 3.把桶中的資料重新合併
        tempDatas = [NSMutableArray array];
        for (int i = 0; i < 10; i++) {
            NSMutableArray *aBucket = buckets[i];
            [tempDatas addObjectsFromArray:aBucket];

        }
        // 4.求出陣列中最大數的位數, 只需計算一次
        if (maxDigit == 0) {
            while(maxValue > 0){
                maxValue = maxValue / 10;
                maxDigit++;
            }
        }
        // 5.繼續下一輪排序
        datas = tempDatas;
        level += 1;

    } while (level < maxDigit);

    return tempDatas;
}

特點

穩定性:在元素拆分的時候,相同元素會被分到同一組中,合併的時候也是按順序合併,故穩定。

空間複雜度:O ( n + k );

時間複雜度:最好最壞都為 O( n * k );

總結

以上就是 iOS 中的十大經典排序演算法,仔細閱讀一番理解之後,能助你在 iOS 的演算法筆試環節一臂之力。