編程排序算法


 

 我們通常所說的排序算法往往指的是內部排序算法,即數據記錄在內存中進行排序。

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

    一種是比較排序,時間復雜度O(nlogn) ~ O(n^2),主要有:冒泡排序選擇排序插入排序歸並排序堆排序快速排序等。

    另一種是非比較排序,時間復雜度可以達到O(n),主要有:計數排序基數排序桶排序等。

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

  

 

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

  排序算法穩定性的簡單形式化定義為:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai還在Aj之前,則稱這種排序算法是穩定的。通俗地講就是保證排序前后兩個相等的數的相對順序不變。

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

  例如,對於冒泡排序,原本是穩定的排序算法,如果將記錄交換的條件改成A[i] >= A[i + 1],則兩個相等的記錄就會交換位置,從而變成不穩定的排序算法。

  其次,說一下排序算法穩定性的好處。排序算法如果是穩定的,那么從一個鍵上排序,然后再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位排序后元素的順序在高位也相同時是不會改變的。

 

 

  冒泡排序(Bubble Sort)

 

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

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

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

  由於它的簡潔,冒泡排序通常被用來對於程序設計入門的學生介紹算法的概念。冒泡排序的代碼如下:

#include <stdio.h>

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間復雜度 ---- O(n^2)
// 最優時間復雜度 ---- 如果能在內部循環第一次運行時,使用一個旗標來表示有無需要交換的可能,可以把最優時間復雜度降低到O(n)
// 平均時間復雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定

void exchange(int A[], int i, int j)        // 交換A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    // 從小到大冒泡排序
    int n = sizeof(A) / sizeof(int);                
    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],則變為不穩定的排序算法
            {
                exchange(A, i, i + 1);        
            }
        }
    }
    printf("冒泡排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

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

    

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

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

 

 

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

 

  雞尾酒排序,也叫定向冒泡排序,是冒泡排序的一種改進。此算法與冒泡排序的不同處在於從低到高然后從高到低,而冒泡排序則僅從低到高去比較序列里的每個元素。他可以得到比冒泡排序稍微好一點的效能。

  雞尾酒排序的代碼如下:

 
#include <stdio.h>

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間復雜度 ---- O(n^2)
// 最優時間復雜度 ---- 如果序列在一開始已經大部分排序過的話,會接近O(n)
// 平均時間復雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定

void exchange(int A[], int i, int j)        // 交換A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };   // 從小到大定向冒泡排序
    int n = sizeof(A) / sizeof(int);                
    int left = 0;                           // 初始化邊界
    int right = n - 1;
    while (left < right)
    {
        for (int i = left; i < right; i++)  // 前半輪,將最大元素放到后面
            if (A[i] > A[i + 1]) 
            {
                exchange(A, i, i + 1);
            }
        right--;
        for (int i = right; i > left; i--)  // 后半輪,將最小元素放到前面
            if (A[i - 1] > A[i]) 
            {
                exchange(A, i - 1, i);
            }
        left++;
    }
    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 exchange(int A[], int i, int j)        // 交換A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int main()
{
    int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 從小到大選擇排序
    int n = sizeof(A) / sizeof(int);
    int i, j, min;
    for (i = 0; i <= n - 2; i++)                // 已排序序列的末尾
    {
        min = i;    
        for (j = i + 1; j <= n - 1; j++)        // 未排序序列
        {
            if (A[j] < A[min])// 依次找出未排序序列中的最小值,存放到已排序序列的末尾
            {
                min = j;
            }
        }
        if (min != i)
        {
            exchange(A, min, i);    // 該操作很有可能把穩定性打亂,所以選擇排序是不穩定的排序算法
        }
    }
    printf("選擇排序結果:");
    for (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)
// 穩定性 ------------ 穩定

int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 從小到大插入排序
    int n = sizeof(A) / sizeof(int);
    int i, j, get;

    for (i = 1; i < n; i++)             // 類似抓撲克牌排序
    {
        get = A[i];                     // 右手抓到一張撲克牌
        j = i - 1;                      // 拿在左手上的牌總是排序好的
        while (j >= 0 && A[j] > get)    // 將抓到的牌與手牌從右向左進行比較
        {
            A[j + 1] = A[j];            // 如果該手牌比抓到的牌大,就將其右移
            j--;
        }
        A[j + 1] = get;// 直到該手牌比抓到的牌小(或二者相等),將抓到的牌插入到該手牌右邊(相等元素的相對次序未變,所以插入排序是穩定的)
    }
    printf("插入排序結果:");
    for (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)
// 穩定性 ------------ 穩定

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

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大希爾排序
    int n = sizeof(A) / sizeof(int);
    int i, j, get;
    int h = 0;
    while (h <= n)                          // 生成初始增量
    {
        h = 3*h + 1;
    }
    while (h >= 1)
    {
        for (i = h; i < n; i++)
        {
            j = i - h;
            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;                    // 遞減增量
    }
    printf("希爾排序結果:");
    for (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>                // 包含極限值的頭文件,這里用到了無窮大INT_MAX

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間復雜度 ---- O(nlogn)
// 最優時間復雜度 ---- O(nlogn)
// 平均時間復雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(n)
// 穩定性 ------------ 穩定

int L[10];    // 兩個子數組定義成全局變量(輔助存儲空間,大小正比於元素的個數)
int R[10];

void merge(int A[], int left, int middle, int right)// 合並兩個已排好序的數組A[left...middle]和A[middle+1...right]
{
    int n1 = middle - left + 1;     // 兩個數組的大小
    int n2 = right - middle;
    for (int i = 0; i < n1; i++)    // 把兩部分分別拷貝到兩個數組中
        L[i] = A[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = A[middle + j + 1];
    L[n1] = INT_MAX;                // 使用無窮大作為哨兵值放在子數組的末尾
    R[n2] = INT_MAX;                // 這樣可以免去檢查某個子數組是否已讀完的步驟
    int i = 0;
    int j = 0;
    for (int k = left; k <= right; k++) // 依次比較兩個子數組中的值,每次取出更小的那一個放入原數組
    {
        if (L[i] <= R[j]) 
        {
            A[k] = L[i];
            i++;
        }
        else
        {
            A[k] = R[j];
            j++;
        }
    }

}

void mergesort_recursion(int A[], int left, int right) // 遞歸實現的歸並排序(自頂向下)
{
    int middle = (left + right) / 2;
    if (left < right)          // 當待排序的序列長度為1時(left == right),遞歸“開始回升”
    {
        mergesort_recursion(A, left, middle);
        mergesort_recursion(A, middle + 1, right);
        merge(A, left, middle, right);
    }
}

void mergesort_iteration(int A[], int left, int right)  // 非遞歸(迭代)實現的歸並排序(自底向上)
{
    int low, middle, high;    // 子數組索引,前一個為A[low...middle],后一個子數組為A[middle+1...high]
    for (int size = 1; size <= right - left; size *= 2) // 子數組的大小初始為1,每輪翻倍
    {
        low = left;
        while (low + size - 1 <= right - 1 )// 后一個子數組存在(需要歸並)
        {
            middle = low + size - 1;    
            high = middle + size;        
            if (high > right)              // 后一個子數組大小不足size
                high = right;
            merge(A, low, middle, high);
            low = high + 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);
    mergesort_recursion(A1, 0, n1 - 1);       // 遞歸實現
    mergesort_iteration(A2, 0, n2 - 1);       // 非遞歸實現
    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 }進行歸並排序的實例如下 

 

     

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

  

 

 

  堆排序(Heapsort)

  

  堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構(通常堆是通過一維數組來實現的),並同時滿足堆的性質:即子結點的鍵值總是小於(或者大於)它的父節點。

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

  1. 創建一個堆
  2. 把堆頂元素(最大值)和堆尾元素互換
  3. 把堆的尺寸縮小1,並調用heapify(A, 0)從新的堆頂元素開始進行堆調整
  4. 重復步驟2,直到堆的尺寸為1

  堆排序的代碼如下:

 
#include <stdio.h>

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間復雜度 ---- O(nlogn)
// 最優時間復雜度 ---- O(nlogn)
// 平均時間復雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定

int heapsize;    // 堆大小

void exchange(int A[], int i, int j)    // 交換A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

void heapify(int A[], int i)            // 堆調整函數(這里使用的是最大堆)
{
    int leftchild = 2 * i + 1;          // 左孩子索引
    int rightchild = 2 * i + 2;         // 右孩子索引
    int largest;                        // 選出當前結點與左右孩子之中的最大值
    if (leftchild < heapsize && A[leftchild] > A[i])
        largest = leftchild;
    else
        largest = i;
    if (rightchild < heapsize && A[rightchild] > A[largest])
        largest = rightchild;
    if (largest != i)                    
    {
        exchange(A, i, largest);        // 把當前結點和它的最大(直接)子節點進行交換
        heapify(A, largest);            // 遞歸調用,繼續從當前結點向下進行堆調整
    }
}

void buildheap(int A[], int n)          // 建堆函數
{
    heapsize = n;
    for (int i = heapsize / 2 - 1; i >= 0; i--) // 對每一個非葉結點
        heapify(A, i);                  // 不斷的堆調整
}

void heapsort(int A[], int n)
{
    buildheap(A, n);
    for (int i = n - 1; i >= 1; i--)
    {
        exchange(A, 0, i); // 將堆頂元素(當前最大值)與堆的最后一個元素互換(該操作很有可能把后面元素的穩定性打亂,所以堆排序是不穩定的排序算法)
        heapsize--;                     // 從堆中去掉最后一個元素
        heapify(A, 0);                  // 從新的堆頂元素開始進行堆調整
    }
}

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的相對次序。

 

 

  快速排序(Quicksort)

 

  快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序n個元素要O(nlogn)次比較。在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他O(nlogn)算法更快,因為它的內部循環可以在大部分的架構上很有效率地被實現出來。

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

  1. 從序列中挑出一個元素,作為"基准"(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的數可以到任一邊),這個稱為分區(partition)操作。
  3. 對每個分區遞歸地進行步驟1~3,遞歸的結束條件是序列的大小是0或1,這時整體已經被排好序了。

  快速排序的代碼如下:

 
#include <stdio.h>

// 分類 ------------ 內部比較排序
// 數據結構 --------- 數組
// 最差時間復雜度 ---- 每次選取的基准都是最大的元素(或者每次都是最小),導致每次只划分出了一個子序列,需要進行n-1次划分才能結束遞歸,時間復雜度為O(n^2)
// 最優時間復雜度 ---- 每次選取的基准都能使划分均勻,只需要logn次划分就能結束遞歸,時間復雜度為O(nlogn)
// 平均時間復雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(logn)~O(n),主要是遞歸造成的棧空間的使用(用來保存left和right等局部變量),取決於遞歸樹的深度
// 一般為O(logn),最差為O(n)(基本有序的情況) // 穩定性 ---------- 不穩定 void exchange(int A[], int i, int j) // 交換A[i]和A[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) // 把小於等於基准的元素放到前一個子數組中 { tail++; exchange(A, tail, i); } } exchange(A, tail + 1, right); // 最后把基准放到前一個子數組的后邊,剩下的子數組既是大於基准的子數組
                          // 該操作很有可能把后面元素的穩定性打亂,所以快速排序是不穩定的排序算法 return tail + 1; // 返回基准的索引 } void quicksort(int A[], int left, int right) { int pivot_index; // 基准的索引 if (left < right) { 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的相對次序。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM