機器學習——經常使用排序算法總結

咱們一般所說的排序算法每每指的是內部排序算法,即數據記錄在內存中進行排序。排序算法大致可分爲兩種:javascript

  • 一種是比較排序,時間複雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序等。
  • 另外一種是非比較排序,時間複雜度能夠達到O(n),主要有:計數排序,基數排序,桶排序等。

 

這裏咱們來探討一下經常使用的比較排序算法,非比較排序算法將在下一篇文章中介紹。下表給出了常見比較排序算法的性能:html

有一點咱們很容易忽略的是排序算法的穩定性(騰訊校招2016筆試題曾考過)。排序算法穩定性的簡單形式化定義爲:若是Ai = Aj,排序前Ai在Aj以前,排序後Ai還在Aj以前,則稱這種排序算法是穩定的。通俗地講就是保證排序先後兩個相等的數的相對順序不變。java

 

對於不穩定的排序算法,只要舉出一個實例,便可說明它的不穩定性;而對於穩定的排序算法,必須對算法進行分析從而獲得穩定的特性。須要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下能夠變爲穩定的算法,而穩定的算法在某種條件下也能夠變爲不穩定的算法。算法

 

例如,對於冒泡排序,本來是穩定的排序算法,若是將記錄交換的條件改爲A[i] >= A[i + 1],則兩個相等的記錄就會交換位置,從而變成不穩定的排序算法。其次,說一下排序算法穩定性的好處。排序算法若是是穩定的,那麼從一個鍵上排序,而後再從另外一個鍵上排序,前一個鍵排序的結果能夠爲後一個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位排序後元素的順序在高位也相同時是不會改變的。api

 

冒泡排序(Bubble Sort)

冒泡排序是一種極其簡單的排序算法,也是我所學的第一個排序算法。 它重複地走訪過要排序的元素,依次比較相鄰兩個元素,若是他們的順序錯誤就把他們調換過來,直到沒有元素再須要交換,排序完成。這個算法的名字由來是由於越小(或越大)的元素會經由交換慢慢「浮」到數列的頂端。冒泡排序算法的運做以下:數組

  1. 比較相鄰的元素,若是前一個比後一個大,就把它們兩個調換位置。
  2. 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。
  3. 針對全部的元素重複以上的步驟,除了最後一個。
  4. 持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。

 

因爲它的簡潔,冒泡排序一般被用來對於程序設計入門的學生介紹算法的概念。冒泡排序的代碼以下:數據結構

#include <stdio.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 若是能在內部循環第一次運行時,使用一個旗標來表示有無須要交換的可能,能夠把最優時間複雜度下降到O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
 
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++)         // 每次最大元素就像氣泡同樣"浮"到數組的最後
    {
        for (int i = 0; i < n - 1 - j; i++) // 依次比較相鄰的兩個元素,使較大的那個向後移
        {
            if (A[i] > A[i + 1])            // 若是條件改爲A[i] >= A[i + 1],則變爲不穩定的排序算法
            {
                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("冒泡排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進行冒泡排序的實現過程以下:架構

 

使用冒泡排序爲一列數字進行排序的過程如右圖所示:ide

 

儘管冒泡排序是最容易瞭解和實現的排序算法之一,但它對於少數元素以外的數列排序是很沒有效率的。函數

 

冒泡排序的改進:雞尾酒排序

雞尾酒排序,也叫定向冒泡排序,是冒泡排序的一種改進。此算法與冒泡排序的不一樣處在於從低到高而後從高到低,而冒泡排序則僅從低到高去比較序列裏的每一個元素。他能夠獲得比冒泡排序稍微好一點的效能。雞尾酒排序的代碼以下:

#include <stdio.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 若是序列在一開始已經大部分排序過的話,會接近O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
 
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("雞尾酒排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

使用雞尾酒排序爲一列數字進行排序的過程如右圖所示:

 

以序列(2,3,4,5,1)爲例,雞尾酒排序只須要訪問一次序列就能夠完成排序,但若是使用冒泡排序則須要四次。可是在亂數序列的狀態下,雞尾酒排序與冒泡排序的效率都不好勁。

 

選擇排序(Selection Sort)

選擇排序也是一種簡單直觀的排序算法。它的工做原理很容易理解:初始時在序列中找到最小(大)元素,放到序列的起始位置做爲已排序序列;而後,再從剩餘未排序元素中繼續尋找最小(大)元素,放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。

注意選擇排序與冒泡排序的區別:冒泡排序經過依次交換相鄰兩個順序不合法的元素位置,從而將當前最小(大)元素放到合適的位置;而選擇排序每遍歷一次都記住了當前最小(大)元素的位置,最後僅需一次交換操做便可將其放到合適的位置。

 

選擇排序的代碼以下:

#include <stdio.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(n^2)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
 
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);    // 放到已排序序列的末尾,該操做頗有可能把穩定性打亂,因此選擇排序是不穩定的排序算法
        }
    }
}
 
int main()
{
    int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 從小到大選擇排序
    int n = sizeof(A) / sizeof(int);
    SelectionSort(A, n);
    printf("選擇排序結果:");
    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 }進行選擇排序的實現過程如右圖:

 

使用選擇排序爲一列數字進行排序的宏觀過程:

 

選擇排序是不穩定的排序算法,不穩定發生在最小元素與A[i]交換的時刻。好比序列:{ 5, 8, 5, 2, 9 },一次選擇的最小元素是2,而後把2和第一個5進行交換,從而改變了兩個元素5的相對次序。

 

插入排序(Insertion Sort)

插入排序是一種簡單直觀的排序算法。它的工做原理很是相似於咱們抓撲克牌

對於未排序數據(右手抓到的牌),在已排序序列(左手已經排好序的手牌)中從後向前掃描,找到相應位置並插入。插入排序在實現上,一般採用in-place排序(即只需用到O(1)的額外空間的排序),於是在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。具體算法描述以下:

  1. 從第一個元素開始,該元素能夠認爲已經被排序

  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描

  3. 若是該元素(已排序)大於新元素,將該元素移到下一位置

  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置

  5. 將新元素插入到該位置後

  6. 重複步驟2~5

 

插入排序的代碼以下:

#include <stdio.h>
 
// 分類 ------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 最壞狀況爲輸入序列是降序排列的,此時時間複雜度O(n^2)
// 最優時間複雜度 ---- 最好狀況爲輸入序列是升序排列的,此時時間複雜度O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
 
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)    // 將抓到的牌與手牌從右向左進行比較
        {
            A[j + 1] = A[j];            // 若是該手牌比抓到的牌大,就將其右移
            j--;
        }
        A[j + 1] = get; // 直到該手牌比抓到的牌小(或兩者相等),將抓到的牌插入到該手牌右邊(相等元素的相對次序未變,因此插入排序是穩定的)
    }
}
 
int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 從小到大插入排序
    int n = sizeof(A) / sizeof(int);
    InsertionSort(A, n);
    printf("插入排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進行插入排序的實現過程以下:

 

使用插入排序爲一列數字進行排序的宏觀過程:

 

插入排序不適合對於數據量比較大的排序應用。可是,若是須要排序的數據量很小,好比量級小於千,那麼插入排序仍是一個不錯的選擇。 插入排序在工業級庫中也有着普遍的應用,在STL的sort算法和stdlib的qsort算法中,都將插入排序做爲快速排序的補充,用於少許元素的排序(一般爲8個或如下)。

 

插入排序的改進:二分插入排序

對於插入排序,若是比較操做的代價比交換操做大的話,能夠採用二分查找法來減小比較操做的次數,咱們稱爲二分插入排序,代碼以下:

#include <stdio.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
 
void InsertionSortDichotomy(int A[], int n)
{
    for (int i = 1; i < n; i++)
    {
        int get = A[i];                    // 右手抓到一張撲克牌
        int left = 0;                    // 拿在左手上的牌老是排序好的,因此能夠用二分法
        int right = i - 1;                // 手牌左右邊界進行初始化
        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("二分插入排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

當n較大時,二分插入排序的比較次數比直接插入排序的最差狀況好得多,但比直接插入排序的最好狀況要差,所當以元素初始序列已經接近升序時,直接插入排序比二分插入排序比較次數少。二分插入排序元素移動次數與直接插入排序相同,依賴於元素初始序列。

 

插入排序的更高效改進:希爾排序(Shell Sort) 希爾排序,也叫遞減增量排序,是插入排序的一種更高效的改進版本。希爾排序是不穩定的排序算法。希爾排序是基於插入排序的如下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操做時,效率高,便可以達到線性排序的效率

  • 但插入排序通常來講是低效的,由於插入排序每次只能將數據移動一位

希爾排序經過將比較的所有元素分爲幾個區域來提高插入排序的性能。這樣可讓一個元素能夠一次性地朝最終位置前進一大步。而後算法再取愈來愈小的步長進行排序,算法的最後一步就是普通的插入排序,可是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。

 

假設有一個很小的數據在一個已按升序排好序的數組的末端。若是用複雜度爲O(n^2)的排序(冒泡排序或直接插入排序),可能會進行n次的比較和交換才能將該數據移至正確位置。而希爾排序會用較大的步長移動數據,因此小數據只需進行少數比較和交換便可到正確位置。

 

希爾排序的代碼以下:

#include <stdio.h>  
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 根據步長序列的不一樣而不一樣。已知最好的爲O(n(logn)^2)
// 最優時間複雜度 ---- O(n)
// 平均時間複雜度 ---- 根據步長序列的不一樣而不一樣。
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
 
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("希爾排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

以23, 10, 4, 1的步長序列進行希爾排序:

希爾排序是不穩定的排序算法,雖然一次插入排序是穩定的,不會改變相同元素的相對順序,但在不一樣的插入排序過程當中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂。好比序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2時分紅兩個子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序以前第二個子序列中的8在前面,如今對兩個子序列進行插入排序,獲得 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,兩個8的相對次序發生了改變。

 

歸併排序(Merge Sort)

歸併排序是建立在歸併操做上的一種有效的排序算法,效率爲O(nlogn),1945年由馮·諾伊曼首次提出。歸併排序的實現分爲遞歸實現與非遞歸(迭代)實現。遞歸實現的歸併排序是算法設計中分治策略的典型應用,咱們將一個大問題分割成小問題分別解決,而後用全部小問題的答案來解決整個大問題。非遞歸(迭代)實現的歸併排序首先進行是兩兩歸併,而後四四歸併,而後是八八歸併,一直下去直到歸併了整個數組。歸併排序算法主要依賴歸併(Merge)操做。歸併操做指的是將兩個已經排序的序列合併成一個序列的操做,歸併操做步驟以下:

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列

  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置

  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置

  4. 重複步驟3直到某一指針到達序列尾

  5. 將另外一序列剩下的全部元素直接複製到合併序列尾

 

歸併排序的代碼以下:

#include <stdio.h>
#include <limits.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(n)
// 穩定性 ------------ 穩定
 
 
void Merge(int A[], int left, int mid, int right)// 合併兩個已排好序的數組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;                   // 前一數組的起始元素
    int j = mid + 1;                // 後一數組的起始元素
    while (i <= mid && j <= right)
    {
        temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];  // 帶等號保證歸併排序的穩定性
    }
    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)    // 遞歸實現的歸併排序(自頂向下)
{
    if (left == right)    // 當待排序的序列長度爲1時,遞歸開始回溯,進行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)    // 非遞歸(迭代)實現的歸併排序(自底向上)
{
    int left, mid, right;// 子數組索引,前一個爲A[left...mid],後一個子數組爲A[mid+1...right]
    for (int i = 1; i < len; i *= 2)        // 子數組的大小i初始爲1,每輪翻倍
    {
        left = 0;
        while (left + i < len)              // 後一個子數組存在(須要歸併)
        {
            mid = left + i - 1;
            right = mid + i < len ? mid + i : len - 1;// 後一個子數組大小可能不夠
            Merge(A, left, mid, right);
            left = right + 1;               // 前一個子數組索引向後移動
        }
    }
}
 
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);          // 遞歸實現
    MergeSortIteration(A2, n2);                 // 非遞歸實現
    printf("遞歸實現的歸併排序結果:");
    for (int i = 0; i < n1; i++)
    {
        printf("%d ", A1[i]);
    }
    printf("\n");
    printf("非遞歸實現的歸併排序結果:");
    for (int i = 0; i < n2; i++)
    {
        printf("%d ", A2[i]);
    }
    printf("\n");
    return 0;
}

 

上述代碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進行歸併排序的實例以下:

 

使用歸併排序爲一列數字進行排序的宏觀過程:

 

歸併排序除了能夠對數組進行排序,還能夠高效的求出數組小和(即單調和)以及數組中的逆序對,詳見這篇博文。

 

堆排序(Heap Sort)

堆排序是指利用堆這種數據結構所設計的一種選擇排序算法。堆是一種近似徹底二叉樹的結構(一般堆是經過一維數組來實現的),並知足性質:以最大堆(也叫大根堆、大頂堆)爲例,其中父結點的值老是大於它的孩子節點。咱們能夠很容易的定義堆排序的過程:

  1. 由輸入的無序數組構造一個最大堆,做爲初始的無序區

  2. 把堆頂元素(最大值)和堆尾元素互換

  3. 把堆(無序區)的尺寸縮小1,並調用heapify(A, 0)重新的堆頂元素開始進行堆調整

  4. 重複步驟2,直到堆的尺寸爲1

 

堆排序的代碼以下:

#include <stdio.h>
 
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
 
 
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]向下進行堆調整
{
    int left_child = 2 * i + 1;         // 左孩子索引
    int right_child = 2 * i + 2;        // 右孩子索引
    int max = i;                        // 選出當前結點與其左右孩子三者之中的最大值
    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);                // 把當前結點和它的最大(直接)子節點進行交換
        Heapify(A, max, size);          // 遞歸調用,繼續從當前結點向下進行堆調整
    }
}
 
int BuildHeap(int A[], int n)           // 建堆,時間複雜度O(n)
{
    int heap_size = n;
    for (int i = heap_size / 2 - 1; i >= 0; i--) // 從每個非葉結點開始向下進行堆調整
        Heapify(A, i, heap_size);
    return heap_size;
}
 
void HeapSort(int A[], int n)
{
    int heap_size = BuildHeap(A, n);    // 創建一個最大堆
    while (heap_size > 1)           // 堆(無序區)元素個數大於1,未完成排序
    {
        // 將堆頂元素與堆的最後一個元素互換,並從堆中去掉最後一個元素
        // 此處交換操做頗有可能把後面元素的穩定性打亂,因此堆排序是不穩定的排序算法
        Swap(A, 0, --heap_size);
        Heapify(A, 0, heap_size);     // 重新的堆頂元素開始向下進行堆調整,時間複雜度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("堆排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

堆排序算法的演示:

 

動畫中在排序過程以前簡單的表現了建立堆的過程以及堆的邏輯結構。堆排序是不穩定的排序算法,不穩定發生在堆頂元素與A[i]交換的時刻。好比序列:{ 9, 5, 7, 5 },堆頂元素是9,堆排序下一步將9和第二個5進行交換,獲得序列 { 5, 5, 7, 9 },再進行堆調整獲得{ 7, 5, 5, 9 },重複以前的操做最後獲得{ 5, 5, 7, 9 }從而改變了兩個5的相對次序。

 

快速排序(Quick Sort)

快速排序是由東尼·霍爾所發展的一種排序算法。在平均情況下,排序n個元素要O(nlogn)次比較。在最壞情況下則須要O(n^2)次比較,但這種情況並不常見。事實上,快速排序一般明顯比其餘O(nlogn)算法更快,由於它的內部循環能夠在大部分的架構上頗有效率地被實現出來。快速排序使用分治策略(Divide and Conquer)來把一個序列分爲兩個子序列。步驟爲:

  1. 從序列中挑出一個元素,做爲」基準」(pivot).

  2. 把全部比基準值小的元素放在基準前面,全部比基準值大的元素放在基準的後面(相同的數能夠到任一邊),這個稱爲分區(partition)操做。

  3. 對每一個分區遞歸地進行步驟1~2,遞歸的結束條件是序列的大小是0或1,這時總體已經被排好序了。

 

快速排序的代碼以下:

#include <stdio.h>
 
// 分類 ------------ 內部比較排序
// 數據結構 --------- 數組
// 最差時間複雜度 ---- 每次選取的基準都是最大(或最小)的元素,致使每次只劃分出了一個分區,須要進行n-1次劃分才能結束遞歸,時間複雜度爲O(n^2)
// 最優時間複雜度 ---- 每次選取的基準都是中位數,這樣每次都均勻的劃分出兩個分區,只須要logn次劃分就能結束遞歸,時間複雜度爲O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ 主要是遞歸形成的棧空間的使用(用來保存left和right等局部變量),取決於遞歸樹的深度,通常爲O(logn),最差爲O(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)  // 劃分函數
{
    int pivot = A[right];               // 這裏每次都選擇最後一個元素做爲基準
    int tail = left - 1;                // tail爲小於基準的子數組最後一個元素的索引
    for (int i = left; i < right; i++)  // 遍歷基準之外的其餘元素
    {
        if (A[i] <= pivot)              // 把小於等於基準的元素放到前一個子數組末尾
        {
            Swap(A, ++tail, i);
        }
    }
    Swap(A, tail + 1, right);           // 最後把基準放到前一個子數組的後邊,剩下的子數組既是大於基準的子數組
                                        // 該操做頗有可能把後面元素的穩定性打亂,因此快速排序是不穩定的排序算法
    return tail + 1;                    // 返回基準的索引
}
 
void QuickSort(int A[], int left, int right)
{
    if (left >= right)
        return;
    int pivot_index = Partition(A, left, right); // 基準的索引
    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("快速排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

使用快速排序法對一列數字進行排序的過程:

快速排序是不穩定的排序算法,不穩定發生在基準元素與A[tail+1]交換的時刻。好比序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基準元素是5,一次劃分操做後5要和第一個8進行交換,從而改變了兩個元素8的相對次序。

 

ava系統提供的Arrays.sort函數。對於基礎類型,底層使用快速排序。對於非基礎類型,底層使用歸併排序。請問是爲何?

 

答:這是考慮到排序算法的穩定性。對於基礎類型,相同值是無差異的,排序先後相同值的相對位置並不重要,因此選擇更爲高效的快速排序,儘管它是不穩定的排序算法;而對於非基礎類型,排序先後相等實例的相對位置不宜改變,因此選擇穩定的歸併排序。

閱讀原文

相關文章
相關標籤/搜索