常用排序算法總結(jié)

大寫的轉(zhuǎn)

目錄

  • [冒泡排序]
    • [雞尾酒排序]
  • [選擇排序]
  • [插入排序]
    • [二分插入排序]
    • [希爾排序]
  • [歸并排序]
  • [堆排序]
  • [快速排序]

我們通常所說的排序算法往往指的是內(nèi)部排序算法家肯,即數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序棉浸。

排序算法大體可分為兩種:

一種是比較排序几缭,時間復(fù)雜度O(nlogn) ~ O(n^2)眼滤,主要有:冒泡排序選擇排序男公,插入排序核芽,歸并排序堆排序磁携,快速排序等。

另一種是非比較排序良风,時間復(fù)雜度可以達(dá)到O(n)谊迄,主要有:計數(shù)排序基數(shù)排序烟央,桶排序等统诺。

這里我們來探討一下常用的比較排序算法,非比較排序算法將在下一篇文章中介紹疑俭。下表給出了常見比較排序算法的性能:

image

有一點我們很容易忽略的是排序算法的穩(wěn)定性(騰訊校招2016筆試題曾考過)粮呢。

排序算法穩(wěn)定性的簡單形式化定義為:如果Ai = Aj,排序前Ai在Aj之前钞艇,排序后Ai還在Aj之前啄寡,則稱這種排序算法是穩(wěn)定的。通俗地講就是保證排序前后兩個相等的數(shù)的相對順序不變香璃。

對于不穩(wěn)定的排序算法这难,只要舉出一個實例,即可說明它的不穩(wěn)定性葡秒;而對于穩(wěn)定的排序算法,必須對算法進(jìn)行分析從而得到穩(wěn)定的特性嵌溢。需要注意的是眯牧,排序算法是否為穩(wěn)定的是由具體算法決定的,不穩(wěn)定的算法在某種條件下可以變?yōu)榉€(wěn)定的算法赖草,而穩(wěn)定的算法在某種條件下也可以變?yōu)椴环€(wěn)定的算法学少。

例如,對于冒泡排序秧骑,原本是穩(wěn)定的排序算法版确,如果將記錄交換的條件改成A[i] >= A[i + 1],則兩個相等的記錄就會交換位置乎折,從而變成不穩(wěn)定的排序算法绒疗。

其次,說一下排序算法穩(wěn)定性的好處骂澄。排序算法如果是穩(wěn)定的吓蘑,那么從一個鍵上排序,然后再從另一個鍵上排序,前一個鍵排序的結(jié)果可以為后一個鍵排序所用磨镶。基數(shù)排序就是這樣溃蔫,先按低位排序,逐次按高位排序琳猫,低位排序后元素的順序在高位也相同時是不會改變的伟叛。

冒泡排序(Bubble Sort)

冒泡排序是一種極其簡單的排序算法,也是我所學(xué)的第一個排序算法脐嫂。它重復(fù)地走訪過要排序的元素统刮,依次比較相鄰兩個元素,如果他們的順序錯誤就把他們調(diào)換過來雹锣,直到?jīng)]有元素再需要交換网沾,排序完成。這個算法的名字由來是因為越小(或越大)的元素會經(jīng)由交換慢慢“浮”到數(shù)列的頂端蕊爵。

冒泡排序算法的運(yùn)作如下:

  1. 比較相鄰的元素辉哥,如果前一個比后一個大,就把它們兩個調(diào)換位置攒射。
  2. 對每一對相鄰元素作同樣的工作醋旦,從開始第一對到結(jié)尾的最后一對。這步做完后会放,最后的元素會是最大的數(shù)饲齐。
  3. 針對所有的元素重復(fù)以上的步驟,除了最后一個咧最。
  4. 持續(xù)每次對越來越少的元素重復(fù)上面的步驟捂人,直到?jīng)]有任何一對數(shù)字需要比較。

由于它的簡潔矢沿,冒泡排序通常被用來對于程序設(shè)計入門的學(xué)生介紹算法的概念滥搭。冒泡排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(n^2) // 最優(yōu)時間復(fù)雜度 ---- 如果能在內(nèi)部循環(huán)第一次運(yùn)行時,使用一個旗標(biāo)來表示有無需要交換的可能,可以把最優(yōu)時間復(fù)雜度降低到O(n) // 平均時間復(fù)雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 穩(wěn)定

void Swap(int A[], int i, int j)
{ int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
} void BubbleSort(int A[], int n)
{ for (int j = 0; j < n - 1; j++)         // 每次最大元素就像氣泡一樣"浮"到數(shù)組的最后
 { for (int i = 0; i < n - 1 - j; i++) // 依次比較相鄰的兩個元素,使較大的那個向后移
 { if (A[i] > A[i + 1])            // 如果條件改成A[i] >= A[i + 1],則變?yōu)椴环€(wěn)定的排序算法
 {
                Swap(A, i, i + 1);
            }
        }
    }
} int main()
{ int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    // 從小到大冒泡排序
    int n = sizeof(A) / sizeof(int);
    BubbleSort(A, n);
    printf("冒泡排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進(jìn)行冒泡排序的實現(xiàn)過程如下

image

使用冒泡排序為一列數(shù)字進(jìn)行排序的過程如右圖所示:  
image

盡管冒泡排序是最容易了解和實現(xiàn)的排序算法之一,但它對于少數(shù)元素之外的數(shù)列排序是很沒有效率的捣鲸。

冒泡排序的改進(jìn):雞尾酒排序

雞尾酒排序瑟匆,也叫定向冒泡排序,是冒泡排序的一種改進(jìn)栽惶。此算法與冒泡排序的不同處在于從低到高然后從高到低愁溜,而冒泡排序則僅從低到高去比較序列里的每個元素。他可以得到比冒泡排序稍微好一點的效能外厂。

雞尾酒排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(n^2) // 最優(yōu)時間復(fù)雜度 ---- 如果序列在一開始已經(jīng)大部分排序過的話,會接近O(n) // 平均時間復(fù)雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 穩(wěn)定

void Swap(int A[], int i, int j)
{ int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
} void CocktailSort(int A[], int n)
{ int left = 0;                            // 初始化邊界
    int right = n - 1; while (left < right)
    { for (int i = left; i < right; i++)   // 前半輪,將最大元素放到后面
 { if (A[i] > A[i + 1])
            {
                Swap(A, i, i + 1);
            }
        }
        right--; for (int i = right; i > left; i--)   // 后半輪,將最小元素放到前面
 { if (A[i - 1] > A[i])
            {
                Swap(A, i - 1, i);
            }
        }
        left++;
    }
} int main()
{ int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };   // 從小到大定向冒泡排序
    int n = sizeof(A) / sizeof(int);
    CocktailSort(A, n);
    printf("雞尾酒排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

使用雞尾酒排序為一列數(shù)字進(jìn)行排序的過程如右圖所示:  
image

以序列(2,3,4,5,1)為例冕象,雞尾酒排序只需要訪問一次序列就可以完成排序,但如果使用冒泡排序則需要四次酣衷。但是在亂數(shù)序列的狀態(tài)下交惯,雞尾酒排序與冒泡排序的效率都很差勁。

  選擇排序(Selection Sort)

選擇排序也是一種簡單直觀的排序算法。它的工作原理很容易理解:初始時在序列中找到最邢(大)元素意荤,放到序列的起始位置作為已排序序列;然后只锻,再從剩余未排序元素中繼續(xù)尋找最芯料瘛(大)元素,放到已排序序列的末尾齐饮。以此類推捐寥,直到所有元素均排序完畢。

注意選擇排序與冒泡排序的區(qū)別:冒泡排序通過依次交換相鄰兩個順序不合法的元素位置祖驱,從而將當(dāng)前最形湛摇(大)元素放到合適的位置;而選擇排序每遍歷一次都記住了當(dāng)前最修嗥А(大)元素的位置乡洼,最后僅需一次交換操作即可將其放到合適的位置。

選擇排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(n^2) // 最優(yōu)時間復(fù)雜度 ---- O(n^2) // 平均時間復(fù)雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 不穩(wěn)定

void Swap(int A[], int i, int j)
{ int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
} void SelectionSort(int A[], int n)
{ for (int i = 0; i < n - 1; i++)         // i為已排序序列的末尾
 { int min = i; for (int j = i + 1; j < n; j++)     // 未排序序列
 { if (A[j] < A[min])              // 找出未排序序列中的最小值
 {
                min = j;
            }
        } if (min != i)
        {
            Swap(A, min, i); // 放到已排序序列的末尾匕坯,該操作很有可能把穩(wěn)定性打亂束昵,所以選擇排序是不穩(wěn)定的排序算法
 }
    }
} int main()
{ int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 從小到大選擇排序
    int n = sizeof(A) / sizeof(int);
    SelectionSort(A, n);
    printf("選擇排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

上述代碼對序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }進(jìn)行選擇排序的實現(xiàn)過程如右圖  
image

使用選擇排序為一列數(shù)字進(jìn)行排序的宏觀過程:  
image

選擇排序是不穩(wěn)定的排序算法,不穩(wěn)定發(fā)生在最小元素與A[i]交換的時刻葛峻。

比如序列:{ 5, 8, 5, 2, 9 }锹雏,一次選擇的最小元素是2,然后把2和第一個5進(jìn)行交換术奖,從而改變了兩個元素5的相對次序礁遵。

插入排序(Insertion Sort)

插入排序是一種簡單直觀的排序算法。它的工作原理非常類似于我們抓撲克牌

image

對于未排序數(shù)據(jù)(右手抓到的牌)采记,在已排序序列(左手已經(jīng)排好序的手牌)中從后向前掃描榛丢,找到相應(yīng)位置并插入。

插入排序在實現(xiàn)上挺庞,通常采用in-place排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中稼病,需要反復(fù)把已排序元素逐步向后挪位选侨,為最新元素提供插入空間。

具體算法描述如下:

  1. 從第一個元素開始然走,該元素可以認(rèn)為已經(jīng)被排序
  2. 取出下一個元素援制,在已經(jīng)排序的元素序列中從后向前掃描
  3. 如果該元素(已排序)大于新元素,將該元素移到下一位置
  4. 重復(fù)步驟3芍瑞,直到找到已排序的元素小于或者等于新元素的位置
  5. 將新元素插入到該位置后
  6. 重復(fù)步驟2~5

插入排序的代碼如下:

#include <stdio.h>

// 分類 ------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- 最壞情況為輸入序列是降序排列的,此時時間復(fù)雜度O(n^2) // 最優(yōu)時間復(fù)雜度 ---- 最好情況為輸入序列是升序排列的,此時時間復(fù)雜度O(n) // 平均時間復(fù)雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 穩(wěn)定

void InsertionSort(int A[], int n)
{ for (int i = 1; i < n; i++)         // 類似抓撲克牌排序
 { int get = A[i];                 // 右手抓到一張撲克牌
        int j = i - 1;                  // 拿在左手上的牌總是排序好的
        while (j >= 0 && A[j] > get)    // 將抓到的牌與手牌從右向左進(jìn)行比較
 {
            A[j + 1] = A[j];            // 如果該手牌比抓到的牌大晨仑,就將其右移
            j--;
        }
        A[j + 1] = get; // 直到該手牌比抓到的牌小(或二者相等),將抓到的牌插入到該手牌右邊(相等元素的相對次序未變,所以插入排序是穩(wěn)定的)
 }
} int main()
{ int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 從小到大插入排序
    int n = sizeof(A) / sizeof(int);
    InsertionSort(A, n);
    printf("插入排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進(jìn)行插入排序的實現(xiàn)過程如下

image

使用插入排序為一列數(shù)字進(jìn)行排序的宏觀過程:  
image

插入排序不適合對于數(shù)據(jù)量比較大的排序應(yīng)用洪己。但是妥凳,如果需要排序的數(shù)據(jù)量很小,比如量級小于千答捕,那么插入排序還是一個不錯的選擇逝钥。 插入排序在工業(yè)級庫中也有著廣泛的應(yīng)用,在STL的sort算法和stdlib的qsort算法中拱镐,都將插入排序作為快速排序的補(bǔ)充艘款,用于少量元素的排序(通常為8個或以下)。

插入排序的改進(jìn):二分插入排序

對于插入排序沃琅,如果比較操作的代價比交換操作大的話哗咆,可以采用二分查找法來減少比較操作的次數(shù),我們稱為二分插入排序益眉,代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(n^2) // 最優(yōu)時間復(fù)雜度 ---- O(nlogn) // 平均時間復(fù)雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 穩(wěn)定

void InsertionSortDichotomy(int A[], int n)
{ for (int i = 1; i < n; i++)
    { int get = A[i];                    // 右手抓到一張撲克牌
        int left = 0;                    // 拿在左手上的牌總是排序好的晌柬,所以可以用二分法
        int right = i - 1;                // 手牌左右邊界進(jìn)行初始化
        while (left <= right)            // 采用二分法定位新牌的位置
 { int mid = (left + right) / 2; if (A[mid] > get)
                right = mid - 1; else left = mid + 1;
        } for (int j = i - 1; j >= left; j--)    // 將欲插入新牌位置右邊的牌整體向右移動一個單位
 {
            A[j + 1] = A[j];
        }
        A[left] = get;                    // 將抓到的牌插入手牌
 }
} int main()
{ int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大二分插入排序
    int n = sizeof(A) / sizeof(int);
    InsertionSortDichotomy(A, n);
    printf("二分插入排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

當(dāng)n較大時,二分插入排序的比較次數(shù)比直接插入排序的最差情況好得多呜叫,但比直接插入排序的最好情況要差空繁,所當(dāng)以元素初始序列已經(jīng)接近升序時,直接插入排序比二分插入排序比較次數(shù)少朱庆。二分插入排序元素移動次數(shù)與直接插入排序相同盛泡,依賴于元素初始序列。

插入排序的更高效改進(jìn):希爾排序(Shell Sort)

希爾排序娱颊,也叫遞減增量排序傲诵,是插入排序的一種更高效的改進(jìn)版本。希爾排序是不穩(wěn)定的排序算法箱硕。

希爾排序是基于插入排序的以下兩點性質(zhì)而提出改進(jìn)方法的:

  • 插入排序在對幾乎已經(jīng)排好序的數(shù)據(jù)操作時拴竹,效率高,即可以達(dá)到線性排序的效率
  • 但插入排序一般來說是低效的剧罩,因為插入排序每次只能將數(shù)據(jù)移動一位

希爾排序通過將比較的全部元素分為幾個區(qū)域來提升插入排序的性能栓拜。這樣可以讓一個元素可以一次性地朝最終位置前進(jìn)一大步。然后算法再取越來越小的步長進(jìn)行排序惠昔,算法的最后一步就是普通的插入排序幕与,但是到了這步,需排序的數(shù)據(jù)幾乎是已排好的了(此時插入排序較快)镇防。
  假設(shè)有一個很小的數(shù)據(jù)在一個已按升序排好序的數(shù)組的末端啦鸣。如果用復(fù)雜度為O(n^2)的排序(冒泡排序或直接插入排序),可能會進(jìn)行n次的比較和交換才能將該數(shù)據(jù)移至正確位置来氧。而希爾排序會用較大的步長移動數(shù)據(jù)诫给,所以小數(shù)據(jù)只需進(jìn)行少數(shù)比較和交換即可到正確位置香拉。

希爾排序的代碼如下:

#include <stdio.h>  

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- 根據(jù)步長序列的不同而不同。已知最好的為O(n(logn)^2) // 最優(yōu)時間復(fù)雜度 ---- O(n) // 平均時間復(fù)雜度 ---- 根據(jù)步長序列的不同而不同中狂。 // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 不穩(wěn)定

void ShellSort(int A[], int n)
{ int h = 0; while (h <= n)                          // 生成初始增量
 {
        h = 3 * h + 1;
    } while (h >= 1)
    { for (int i = h; i < n; i++)
        { int j = i - h; int get = A[i]; while (j >= 0 && A[j] > get)
            {
                A[j + h] = A[j];
                j = j - h;
            }
            A[j + h] = get;
        }
        h = (h - 1) / 3;                    // 遞減增量
 }
} int main()
{ int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大希爾排序
    int n = sizeof(A) / sizeof(int);
    ShellSort(A, n);
    printf("希爾排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

以23, 10, 4, 1的步長序列進(jìn)行希爾排序:  
image

希爾排序是不穩(wěn)定的排序算法凫碌,雖然一次插入排序是穩(wěn)定的,不會改變相同元素的相對順序吃型,但在不同的插入排序過程中证鸥,相同的元素可能在各自的插入排序中移動,最后其穩(wěn)定性就會被打亂勤晚。

比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 }枉层,h=2時分成兩個子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二個子序列中的8在前面赐写,現(xiàn)在對兩個子序列進(jìn)行插入排序鸟蜡,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } 挺邀,兩個8的相對次序發(fā)生了改變揉忘。

歸并排序(Merge Sort)

歸并排序是創(chuàng)建在歸并操作上的一種有效的排序算法,效率為O(nlogn)端铛,1945年由馮·諾伊曼首次提出泣矛。

歸并排序的實現(xiàn)分為遞歸實現(xiàn)非遞歸(迭代)實現(xiàn)。遞歸實現(xiàn)的歸并排序是算法設(shè)計中分治策略的典型應(yīng)用禾蚕,我們將一個大問題分割成小問題分別解決您朽,然后用所有小問題的答案來解決整個大問題。非遞歸(迭代)實現(xiàn)的歸并排序首先進(jìn)行是兩兩歸并换淆,然后四四歸并哗总,然后是八八歸并,一直下去直到歸并了整個數(shù)組倍试。

歸并排序算法主要依賴歸并(Merge)操作讯屈。歸并操作指的是將兩個已經(jīng)排序的序列合并成一個序列的操作,歸并操作步驟如下:

  1. 申請空間县习,使其大小為兩個已經(jīng)排序序列之和涮母,該空間用來存放合并后的序列
  2. 設(shè)定兩個指針,最初位置分別為兩個已經(jīng)排序序列的起始位置
  3. 比較兩個指針?biāo)赶虻脑卦暝福x擇相對小的元素放入到合并空間哈蝇,并移動指針到下一位置
  4. 重復(fù)步驟3直到某一指針到達(dá)序列尾
  5. 將另一序列剩下的所有元素直接復(fù)制到合并序列尾

歸并排序的代碼如下:

#include <stdio.h> #include <limits.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(nlogn) // 最優(yōu)時間復(fù)雜度 ---- O(nlogn) // 平均時間復(fù)雜度 ---- O(nlogn) // 所需輔助空間 ------ O(n) // 穩(wěn)定性 ------------ 穩(wěn)定

void Merge(int A[], int left, int mid, int right)// 合并兩個已排好序的數(shù)組A[left...mid]和A[mid+1...right]
{ int len = right - left + 1; int *temp = new int[len];       // 輔助空間O(n)
    int index = 0; int i = left;                   // 前一數(shù)組的起始元素
    int j = mid + 1;                // 后一數(shù)組的起始元素
    while (i <= mid && j <= right)
    {
        temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];  // 帶等號保證歸并排序的穩(wěn)定性
 } while (i <= mid)
    {
        temp[index++] = A[i++];
    } while (j <= right)
    {
        temp[index++] = A[j++];
    } for (int k = 0; k < len; k++)
    {
        A[left++] = temp[k];
    }
} void MergeSortRecursion(int A[], int left, int right)    // 遞歸實現(xiàn)的歸并排序(自頂向下)
{ if (left == right)    // 當(dāng)待排序的序列長度為1時,遞歸開始回溯攘已,進(jìn)行merge操作
        return; int mid = (left + right) / 2;
    MergeSortRecursion(A, left, mid);
    MergeSortRecursion(A, mid + 1, right);
    Merge(A, left, mid, right);
} void MergeSortIteration(int A[], int len)    // 非遞歸(迭代)實現(xiàn)的歸并排序(自底向上)
{ int left, mid, right;// 子數(shù)組索引,前一個為A[left...mid],后一個子數(shù)組為A[mid+1...right]
    for (int i = 1; i < len; i *= 2)        // 子數(shù)組的大小i初始為1怜跑,每輪翻倍
 {
        left = 0; while (left + i < len)              // 后一個子數(shù)組存在(需要歸并)
 {
            mid = left + i - 1;
            right = mid + i < len ? mid + i : len - 1;// 后一個子數(shù)組大小可能不夠
 Merge(A, left, mid, right);
            left = right + 1;               // 前一個子數(shù)組索引向后移動
 }
    }
} int main()
{ int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 };      // 從小到大歸并排序
    int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; int n1 = sizeof(A1) / sizeof(int); int n2 = sizeof(A2) / sizeof(int);
    MergeSortRecursion(A1, 0, n1 - 1);          // 遞歸實現(xiàn)
    MergeSortIteration(A2, n2);                 // 非遞歸實現(xiàn)
    printf("遞歸實現(xiàn)的歸并排序結(jié)果:"); for (int i = 0; i < n1; i++)
    {
        printf("%d ", A1[i]);
    }
    printf("\n");
    printf("非遞歸實現(xiàn)的歸并排序結(jié)果:"); for (int i = 0; i < n2; i++)
    {
        printf("%d ", A2[i]);
    }
    printf("\n"); return 0;
}

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進(jìn)行歸并排序的實例如下

image

使用歸并排序為一列數(shù)字進(jìn)行排序的宏觀過程:  
image

歸并排序除了可以對數(shù)組進(jìn)行排序样勃,還可以高效的求出數(shù)組小和(即單調(diào)和)以及數(shù)組中的逆序?qū)Ψ涂保斠娺@篇博文

堆排序(Heap Sort)

堆排序是指利用堆這種數(shù)據(jù)結(jié)構(gòu)所設(shè)計的一種選擇排序算法峡眶。堆是一種近似完全二叉樹的結(jié)構(gòu)(通常堆是通過一維數(shù)組來實現(xiàn)的)剧防,并滿足性質(zhì):以最大堆(也叫大根堆、大頂堆)為例辫樱,其中父結(jié)點的值總是大于它的孩子節(jié)點峭拘。

我們可以很容易的定義堆排序的過程:

  1. 由輸入的無序數(shù)組構(gòu)造一個最大堆,作為初始的無序區(qū)
  2. 把堆頂元素(最大值)和堆尾元素互換
  3. 把堆(無序區(qū))的尺寸縮小1狮暑,并調(diào)用heapify(A, 0)從新的堆頂元素開始進(jìn)行堆調(diào)整
  4. 重復(fù)步驟2鸡挠,直到堆的尺寸為1

堆排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組 // 最差時間復(fù)雜度 ---- O(nlogn) // 最優(yōu)時間復(fù)雜度 ---- O(nlogn) // 平均時間復(fù)雜度 ---- O(nlogn) // 所需輔助空間 ------ O(1) // 穩(wěn)定性 ------------ 不穩(wěn)定

void Swap(int A[], int i, int j)
{ int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
} void Heapify(int A[], int i, int size)  // 從A[i]向下進(jìn)行堆調(diào)整
{ int left_child = 2 * i + 1;         // 左孩子索引
    int right_child = 2 * i + 2;        // 右孩子索引
    int max = i;                        // 選出當(dāng)前結(jié)點與其左右孩子三者之中的最大值
    if (left_child < size && A[left_child] > A[max])
        max = left_child; if (right_child < size && A[right_child] > A[max])
        max = right_child; if (max != i)
    {
        Swap(A, i, max); // 把當(dāng)前結(jié)點和它的最大(直接)子節(jié)點進(jìn)行交換
        Heapify(A, max, size);          // 遞歸調(diào)用,繼續(xù)從當(dāng)前結(jié)點向下進(jìn)行堆調(diào)整
 }
} int BuildHeap(int A[], int n)           // 建堆搬男,時間復(fù)雜度O(n)
{ int heap_size = n; for (int i = heap_size / 2 - 1; i >= 0; i--) // 從每一個非葉結(jié)點開始向下進(jìn)行堆調(diào)整
 Heapify(A, i, heap_size); return heap_size;
} void HeapSort(int A[], int n)
{ int heap_size = BuildHeap(A, n);    // 建立一個最大堆
    while (heap_size > 1)           // 堆(無序區(qū))元素個數(shù)大于1拣展,未完成排序
 { // 將堆頂元素與堆的最后一個元素互換,并從堆中去掉最后一個元素 // 此處交換操作很有可能把后面元素的穩(wěn)定性打亂缔逛,所以堆排序是不穩(wěn)定的排序算法
        Swap(A, 0, --heap_size);
        Heapify(A, 0, heap_size);     // 從新的堆頂元素開始向下進(jìn)行堆調(diào)整备埃,時間復(fù)雜度O(logn)
 }
} int main()
{ int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大堆排序
    int n = sizeof(A) / sizeof(int);
    HeapSort(A, n);
    printf("堆排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

堆排序算法的演示:  
image

動畫中在排序過程之前簡單的表現(xiàn)了創(chuàng)建堆的過程以及堆的邏輯結(jié)構(gòu)。

  堆排序是不穩(wěn)定的排序算法褐奴,不穩(wěn)定發(fā)生在堆頂元素與A[i]交換的時刻按脚。

比如序列:{ 9, 5, 7, 5 },堆頂元素是9敦冬,堆排序下一步將9和第二個5進(jìn)行交換辅搬,得到序列 { 5, 5, 7, 9 },再進(jìn)行堆調(diào)整得到{ 7, 5, 5, 9 }匪补,重復(fù)之前的操作最后得到{ 5, 5, 7, 9 }從而改變了兩個5的相對次序伞辛。

快速排序(Quick Sort)

快速排序是由東尼·霍爾所發(fā)展的一種排序算法。在平均狀況下夯缺,排序n個元素要O(nlogn)次比較蚤氏。在最壞狀況下則需要O(n^2)次比較,但這種狀況并不常見踊兜。事實上竿滨,快速排序通常明顯比其他O(nlogn)算法更快,因為它的內(nèi)部循環(huán)可以在大部分的架構(gòu)上很有效率地被實現(xiàn)出來捏境。

快速排序使用分治策略(Divide and Conquer)來把一個序列分為兩個子序列于游。步驟為:

  1. 從序列中挑出一個元素,作為"基準(zhǔn)"(pivot).
  2. 把所有比基準(zhǔn)值小的元素放在基準(zhǔn)前面垫言,所有比基準(zhǔn)值大的元素放在基準(zhǔn)的后面(相同的數(shù)可以到任一邊)贰剥,這個稱為分區(qū)(partition)操作。
  3. 對每個分區(qū)遞歸地進(jìn)行步驟1~2筷频,遞歸的結(jié)束條件是序列的大小是0或1蚌成,這時整體已經(jīng)被排好序了前痘。

快速排序的代碼如下:

#include <stdio.h>

// 分類 ------------ 內(nèi)部比較排序 // 數(shù)據(jù)結(jié)構(gòu) --------- 數(shù)組 // 最差時間復(fù)雜度 ---- 每次選取的基準(zhǔn)都是最大(或最小)的元素担忧,導(dǎo)致每次只劃分出了一個分區(qū)芹缔,需要進(jìn)行n-1次劃分才能結(jié)束遞歸,時間復(fù)雜度為O(n^2) // 最優(yōu)時間復(fù)雜度 ---- 每次選取的基準(zhǔn)都是中位數(shù)瓶盛,這樣每次都均勻的劃分出兩個分區(qū)最欠,只需要logn次劃分就能結(jié)束遞歸,時間復(fù)雜度為O(nlogn) // 平均時間復(fù)雜度 ---- O(nlogn) // 所需輔助空間 ------ 主要是遞歸造成的棾兔ǎ空間的使用(用來保存left和right等局部變量)芝硬,取決于遞歸樹的深度,一般為O(logn)帆锋,最差為O(n) // 穩(wěn)定性 ---------- 不穩(wěn)定

void Swap(int A[], int i, int j)
{ int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
} int Partition(int A[], int left, int right)  // 劃分函數(shù)
{ int pivot = A[right];               // 這里每次都選擇最后一個元素作為基準(zhǔn)
    int tail = left - 1;                // tail為小于基準(zhǔn)的子數(shù)組最后一個元素的索引
    for (int i = left; i < right; i++)  // 遍歷基準(zhǔn)以外的其他元素
 { if (A[i] <= pivot)              // 把小于等于基準(zhǔn)的元素放到前一個子數(shù)組末尾
 {
            Swap(A, ++tail, i);
        }
    }
    Swap(A, tail + 1, right);           // 最后把基準(zhǔn)放到前一個子數(shù)組的后邊吵取,剩下的子數(shù)組既是大于基準(zhǔn)的子數(shù)組 // 該操作很有可能把后面元素的穩(wěn)定性打亂,所以快速排序是不穩(wěn)定的排序算法
    return tail + 1;                    // 返回基準(zhǔn)的索引
} void QuickSort(int A[], int left, int right)
{ if (left >= right) return; int pivot_index = Partition(A, left, right); // 基準(zhǔn)的索引
    QuickSort(A, left, pivot_index - 1);
    QuickSort(A, pivot_index + 1, right);
} int main()
{ int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 從小到大快速排序
    int n = sizeof(A) / sizeof(int);
    QuickSort(A, 0, n - 1);
    printf("快速排序結(jié)果:"); for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n"); return 0;
}

使用快速排序法對一列數(shù)字進(jìn)行排序的過程:  
image

快速排序是不穩(wěn)定的排序算法锯厢,不穩(wěn)定發(fā)生在基準(zhǔn)元素與A[tail+1]交換的時刻皮官。

比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基準(zhǔn)元素是5实辑,一次劃分操作后5要和第一個8進(jìn)行交換捺氢,從而改變了兩個元素8的相對次序。

Java系統(tǒng)提供的Arrays.sort函數(shù)剪撬。對于基礎(chǔ)類型摄乒,底層使用快速排序。對于非基礎(chǔ)類型残黑,底層使用歸并排序馍佑。請問是為什么?

答:這是考慮到排序算法的穩(wěn)定性梨水。對于基礎(chǔ)類型拭荤,相同值是無差別的,排序前后相同值的相對位置并不重要疫诽,所以選擇更為高效的快速排序舅世,盡管它是不穩(wěn)定的排序算法;而對于非基礎(chǔ)類型奇徒,排序前后相等實例的相對位置不宜改變雏亚,所以選擇穩(wěn)定的歸并排序。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摩钙,一起剝皮案震驚了整個濱河市罢低,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胖笛,老刑警劉巖奕短,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宜肉,死亡現(xiàn)場離奇詭異,居然都是意外死亡翎碑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門之斯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來日杈,“玉大人,你說我怎么就攤上這事佑刷±蚯埽” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵瘫絮,是天一觀的道長涨冀。 經(jīng)常有香客問我,道長麦萤,這世上最難降的妖魔是什么鹿鳖? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮壮莹,結(jié)果婚禮上翅帜,老公的妹妹穿的比我還像新娘。我一直安慰自己命满,他們只是感情好涝滴,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胶台,像睡著了一般歼疮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诈唬,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天韩脏,我揣著相機(jī)與錄音,去河邊找鬼讯榕。 笑死骤素,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的愚屁。 我是一名探鬼主播济竹,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼霎槐!你這毒婦竟也來了送浊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤丘跌,失蹤者是張志新(化名)和其女友劉穎袭景,沒想到半個月后唁桩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡耸棒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年荒澡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片与殃。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡单山,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出幅疼,到底是詐尸還是另有隱情米奸,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布爽篷,位于F島的核電站悴晰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逐工。R本人自食惡果不足惜铡溪,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钻弄。 院中可真熱鬧佃却,春花似錦姜钳、人聲如沸漾月。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘤泪。三九已至灶泵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間对途,已是汗流浹背赦邻。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留实檀,地道東北人惶洲。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像膳犹,于是被迫代替她去往敵國和親恬吕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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