排序算法

  • 冒泡排序
  • 選擇排序
  • 插入排序
  • 歸并排序
  • 堆排序
  • 快速排序

排序算法大體可分為兩種:
??一種是比較排序,時間復(fù)雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸并排序,堆排序,快速排序等。
??另一種是非比較排序,時間復(fù)雜度可以達到O(n),主要有:計數(shù)排序,基數(shù)排序,桶排序等。

常見比較排序算法的性能:

739525-20160503202729044-614991035.jpg
1. 冒泡排序(Bubble Sort)

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

??冒泡排序算法的運作如下:

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

冒泡排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內(nèi)部比較排序
// 數(shù)據(jù)結(jié)構(gòu) ---------- 數(shù)組
// 最差時間復(fù)雜度 ---- O(n^2)
// 最優(yōu)時間復(fù)雜度 ---- 如果能在內(nèi)部循環(huá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 }進行冒泡排序的實現(xiàn)過程如下:

739525-20160329100443676-1647340243.gif
2. 選擇排序(Selection Sort)

??選擇排序也是一種簡單直觀的排序算法。它的工作原理很容易理解:初始時在序列中找到最?。ù螅┰兀诺叫蛄械钠鹗嘉恢米鳛橐雅判蛐蛄?;然后,再從剩余未排序元素中繼續(xù)尋找最小(大)元素,放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
??注意選擇排序與冒泡排序的區(qū)別:冒泡排序通過依次交換相鄰兩個順序不合法的元素位置,從而將當(dāng)前最?。ù螅┰胤诺胶线m的位置;而選擇排序每遍歷一次都記住了當(dāng)前最?。ù螅┰氐奈恢茫詈髢H需一次交換操作即可將其放到合適的位置。

選擇排序的代碼如下:

 #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;
}

選擇排序過程如下圖:


739525-20160329102006082-273282321.gif

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

3. 插入排序(Insertion Sort)

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

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

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

具體算法描述如下:

  1. 從第一個元素開始,該元素可以認為已經(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)    // 將抓到的牌與手牌從右向左進行比較
        {
            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 }進行插入排序的實現(xiàn)過程如下:


739525-20160329095145504-1018443290.gif

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

4. 歸并排序(Merge Sort)

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

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

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

  1. 申請空間,使其大小為兩個已經(jīng)排序序列之和,該空間用來存放合并后的序列
  2. 設(shè)定兩個指針,最初位置分別為兩個已經(jīng)排序序列的起始位置
  3. 比較兩個指針?biāo)赶虻脑兀x擇相對小的元素放入到合并空間,并移動指針到下一位置
  4. 重復(fù)步驟3直到某一指針到達序列尾
  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時,遞歸開始回溯,進行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 }進行歸并排序的實例如下:

739525-20160328211743473-909317024.gif
5. 堆排序(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)從新的堆頂元素開始進行堆調(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]向下進行堆調(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é)點進行交換
        Heapify(A, max, size);          // 遞歸調(diào)用,繼續(xù)從當(dāng)前結(jié)點向下進行堆調(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é)點開始向下進行堆調(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);     // 從新的堆頂元素開始向下進行堆調(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;
}

堆排序算法的演示:


739525-20160328213839160-2037856208.gif

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

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

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)操作。

  1. 對每個分區(qū)遞歸地進行步驟1~2,遞歸的結(jié)束條件是序列的大小是0或1,這時整體已經(jīng)被排好序了。
      
    快速排序的代碼如下:
#include <stdio.h>

// 分類 ------------ 內(nèi)部比較排序
// 數(shù)據(jù)結(jié)構(gòu) --------- 數(shù)組
// 最差時間復(fù)雜度 ---- 每次選取的基準(zhǔn)都是最大(或最小)的元素,導(dǎo)致每次只劃分出了一個分區(qū),需要進行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ù)字進行排序的過程:

739525-20160328215109269-23458370.gif

??快速排序是不穩(wěn)定的排序算法,不穩(wěn)定發(fā)生在基準(zhǔn)元素與A[tail+1]交換的時刻。
??比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基準(zhǔn)元素是5,一次劃分操作后5要和第一個8進行交換,從而改變了兩個元素8的相對次序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,301評論 0 52
  • 概述:排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,826評論 0 15
  • 大寫的轉(zhuǎn) 目錄 [冒泡排序][雞尾酒排序] [選擇排序] [插入排序][二分插入排序][希爾排序] [歸并排序] ...
    Solang閱讀 1,868評論 0 16
  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    閑云清煙閱讀 820評論 0 6
  • 做朋友圈營銷,產(chǎn)品首先是關(guān)鍵,這是一切銷售應(yīng)該遵循的原則,好的產(chǎn)品和大品牌是一個職業(yè)微商人的首選,靠的就是大品牌好...
    tihuguanding閱讀 620評論 0 0

友情鏈接更多精彩內(nèi)容