常用排序算法總結(二)


   目錄

 

  上一篇文章中我們總結了常用的比較排序算法,主要有冒泡排序選擇排序插入排序歸並排序堆排序快速排序等。

  這篇文章中我們來探討一下常用的非比較排序算法:計數排序基數排序桶排序。在一定條件下,它們的時間復雜度可以達到O(n)。

  這里我們用到的唯一數據結構就是數組,當然我們也可以利用鏈表來實現下述算法。

 

 

  計數排序(Counting Sort)

 

  計數排序用到一個額外的計數數組C,根據數組C來將原數組A中的元素排到正確的位置。

  通俗地理解,例如有10個年齡不同的人,假如統計出有8個人的年齡不比小明大(即小於等於小明的年齡,這里也包括了小明),那么小明的年齡就排在第8位,通過這種思想可以確定每個人的位置,也就排好了序。當然,年齡一樣時需要特殊處理(保證穩定性):通過反向填充目標數組,填充完畢后將對應的數字統計遞減,可以確保計數排序的穩定性。

  計數排序的步驟如下:

  1. 統計數組A中每個值A[i]出現的次數,存入C[A[i]]
  2. 從前向后,使數組C中的每個值等於其與前一項相加,這樣數組C[A[i]]就變成了代表數組A中小於等於A[i]的元素個數
  3. 反向填充目標數組B:將數組元素A[i]放在數組B的第C[A[i]]個位置(下標為C[A[i]] - 1),每放一個元素就將C[A[i]]遞減

  計數排序的實現代碼如下:

#include<iostream>
using namespace std;

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


const int k = 100;   // 基數為100,排序[0,99]內的整數
int C[k];            // 計數數組

void CountingSort(int A[], int n)
{
    for (int i = 0; i < k; i++)   // 初始化,將數組C中的元素置0(此步驟可省略,整型數組元素默認值為0)
    {
        C[i] = 0;
    }
    for (int i = 0; i < n; i++)   // 使C[i]保存着等於i的元素個數
    {
        C[A[i]]++;
    }
    for (int i = 1; i < k; i++)   // 使C[i]保存着小於等於i的元素個數,排序后元素i就放在第C[i]個輸出位置上
    {
        C[i] = C[i] + C[i - 1];
    }
    int *B = (int *)malloc((n) * sizeof(int));// 分配臨時空間,長度為n,用來暫存中間數據
    for (int i = n - 1; i >= 0; i--)    // 從后向前掃描保證計數排序的穩定性(重復元素相對次序不變)
    {
        B[--C[A[i]]] = A[i];      // 把每個元素A[i]放到它在輸出數組B中的正確位置上
                                  // 當再遇到重復元素時會被放在當前元素的前一個位置上保證計數排序的穩定性
    }
    for (int i = 0; i < n; i++)   // 把臨時空間B中的數據拷貝回A
    {
        A[i] = B[i];
    }
    free(B);    // 釋放臨時空間 
}

int main()
{
    int A[] = { 15, 22, 19, 46, 27, 73, 1, 19, 8 };  // 針對計數排序設計的輸入,每一個元素都在[0,100]上且有重復元素
    int n = sizeof(A) / sizeof(int);
    CountingSort(A, n);
    printf("計數排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

  下圖給出了對{ 4, 1, 3, 4, 3 }進行計數排序的簡單演示過程

  

  計數排序的時間復雜度和空間復雜度與數組A的數據范圍(A中元素的最大值與最小值的差加上1)有關,因此對於數據范圍很大的數組,計數排序需要大量時間和內存。

  例如:對0到99之間的數字進行排序,計數排序是最好的算法,然而計數排序並不適合按字母順序排序人名,將計數排序用在基數排序算法中,能夠更有效的排序數據范圍很大的數組。

 

 

  基數排序(Radix Sort)

 

  基數排序的發明可以追溯到1887年赫爾曼·何樂禮在打孔卡片制表機上的貢獻。它是這樣實現的:將所有待比較正整數統一為同樣的數位長度,數位較短的數前面補零。然后,從最低位開始進行基數為10的計數排序,一直到最高位計數排序完后,數列就變成一個有序序列(利用了計數排序的穩定性)。

  基數排序的實現代碼如下:

#include<iostream>
using namespace std;

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

const int dn = 3;                // 待排序的元素為三位數及以下
const int k = 10;                // 基數為10,每一位的數字都是[0,9]內的整數
int C[k];

int GetDigit(int x, int d)          // 獲得元素x的第d位數字
{
    int radix[] = { 1, 1, 10, 100 };// 最大為三位數,所以這里只要到百位就滿足了
    return (x / radix[d]) % 10;
}

void CountingSort(int A[], int n, int d)// 依據元素的第d位數字,對A數組進行計數排序
{
    for (int i = 0; i < k; i++)
    {
        C[i] = 0;
    }
    for (int i = 0; i < n; i++)
    {
        C[GetDigit(A[i], d)]++;
    }
    for (int i = 1; i < k; i++)
    {
        C[i] = C[i] + C[i - 1];
    }
    int *B = (int*)malloc(n * sizeof(int));
    for (int i = n - 1; i >= 0; i--)
    {
        int dight = GetDigit(A[i], d);  // 元素A[i]當前位數字為dight   
        B[--C[dight]] = A[i];           // 根據當前位數字,把每個元素A[i]放到它在輸出數組B中的正確位置上
        // 當再遇到當前位數字同為dight的元素時,會將其放在當前元素的前一個位置上保證計數排序的穩定性
    }
    for (int i = 0; i < n; i++)
    {
        A[i] = B[i];
    }
    free(B);
}

void LsdRadixSort(int A[], int n)     // 最低位優先基數排序
{
    for (int d = 1; d <= dn; d++)     // 從低位到高位
        CountingSort(A, n, d);        // 依據第d位數字對A進行計數排序
}

int main()
{
    int A[] = { 20, 90, 64, 289, 998, 365, 852, 123, 789, 456 };// 針對基數排序設計的輸入
    int n = sizeof(A) / sizeof(int);
    LsdRadixSort(A, n);
    printf("基數排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

  下圖給出了對{ 329, 457, 657, 839, 436, 720, 355 }進行基數排序的簡單演示過程

  

  基數排序的時間復雜度是O(n * dn),其中n是待排序元素個數,dn是數字位數。這個時間復雜度不一定優於O(n log n),dn的大小取決於數字位的選擇(比如比特位數),和待排序數據所屬數據類型的全集的大小;dn決定了進行多少輪處理,而n是每輪處理的操作數目。

  如果考慮和比較排序進行對照,基數排序的形式復雜度雖然不一定更小,但由於不進行比較,因此其基本操作的代價較小,而且如果適當的選擇基數,dn一般不大於log n,所以基數排序一般要快過基於比較的排序,比如快速排序。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序並不是只能用於整數排序。

 

 

  桶排序(Bucket Sort)

 

  桶排序也叫箱排序。工作的原理是將數組元素映射到有限數量個桶里,利用計數排序可以定位桶的邊界,每個桶再各自進行桶內排序(使用其它排序算法或以遞歸方式繼續使用桶排序)。

  桶排序的實現代碼如下:

#include<iostream>
using namespace std;

// 分類 ------------- 內部非比較排序
// 數據結構 --------- 數組
// 最差時間復雜度 ---- O(nlogn)或O(n^2),只有一個桶,取決於桶內排序方式
// 最優時間復雜度 ---- O(n),每個元素占一個桶
// 平均時間復雜度 ---- O(n),保證各個桶內元素個數均勻即可
// 所需輔助空間 ------ O(n + bn)
// 穩定性 ----------- 穩定

/* 本程序用數組模擬桶 */
const int bn = 5;    // 這里排序[0,49]的元素,使用5個桶就夠了,也可以根據輸入動態確定桶的數量
int C[bn];           // 計數數組,存放桶的邊界信息

void InsertionSort(int A[], int left, int right)
{
    for (int i = left + 1; i <= right; i++)  // 從第二張牌開始抓,直到最后一張牌
    {
        int get = A[i];
        int j = i - 1;
        while (j >= left && A[j] > get)
        {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = get;
    }
}

int MapToBucket(int x)
{
    return x / 10;    // 映射函數f(x),作用相當於快排中的Partition,把大量數據分割成基本有序的數據塊
}

void CountingSort(int A[], int n)
{
    for (int i = 0; i < bn; i++)
    {
        C[i] = 0;
    }
    for (int i = 0; i < n; i++)     // 使C[i]保存着i號桶中元素的個數
    {
        C[MapToBucket(A[i])]++;
    }
    for (int i = 1; i < bn; i++)    // 定位桶邊界:初始時,C[i]-1為i號桶最后一個元素的位置
    {
        C[i] = C[i] + C[i - 1];
    }
    int *B = (int *)malloc((n) * sizeof(int));
    for (int i = n - 1; i >= 0; i--)// 從后向前掃描保證計數排序的穩定性(重復元素相對次序不變)
    {
        int b = MapToBucket(A[i]);  // 元素A[i]位於b號桶
        B[--C[b]] = A[i];           // 把每個元素A[i]放到它在輸出數組B中的正確位置上
                                    // 桶的邊界被更新:C[b]為b號桶第一個元素的位置
    }
    for (int i = 0; i < n; i++)
    {
        A[i] = B[i];
    }
    free(B);
}

void BucketSort(int A[], int n)
{
    CountingSort(A, n);          // 利用計數排序確定各個桶的邊界(分桶)
    for (int i = 0; i < bn; i++) // 對每一個桶中的元素應用插入排序
    {
        int left = C[i];         // C[i]為i號桶第一個元素的位置
        int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1為i號桶最后一個元素的位置
        if (left < right)        // 對元素個數大於1的桶進行桶內插入排序
            InsertionSort(A, left, right);
    }
}

int main()
{
    int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 針對桶排序設計的輸入
    int n = sizeof(A) / sizeof(int);
    BucketSort(A, n);
    printf("桶排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

   下圖給出了對{ 29, 25, 3, 49, 9, 37, 21, 43 }進行桶排序的簡單演示過程

  

 

  桶排序不是比較排序,不受到O(nlogn)下限的影響,它是鴿巢排序的一種歸納結果,當所要排序的數組值分散均勻的時候,桶排序擁有線性的時間復雜度。

 


免責聲明!

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



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