【演算法】基數排序

HelloZEX發表於2018-08-06

原文:http://www.cnblogs.com/Braveliu/archive/2013/01/21/2870201.html


程式設計論到極致,核心非程式碼,即思想。

所以,真正的程式設計高手同時是思想獨到及富有智慧(注意與聰明區別)的人。

每一個演算法都是一種智慧的凝聚或萃取,值得我們學習從而提高自己,開拓思路,更重要的是轉換思維角度。

其實,我們大多數人都活在“預設狀態”下。沒有發覺自己的獨特可設定選項-----思想。

言歸正傳(呵呵!恢復預設狀態),以下學習基數排序。

【1】基數排序

以前研究的各種排序演算法,都是通過比較資料大小的方法對欲排資料序列進行排序整理過程。

而基數排序卻不再相同,那麼,基數排序是採用怎樣的策略進行排序的呢?

簡略概述:基數排序是通過“分配”和“收集”過程來實現排序。而這個思想該如何理解呢?請看以下例子。

(1)假設有欲排資料序列如下所示:

73  22  93  43  55  14  28  65  39  81

首先根據個位數的數值,在遍歷資料時將它們各自分配到編號0至9的桶(個位數值與桶號一一對應)中。

分配結果(邏輯想象)如下圖所示:

分配結束後。接下來將所有桶中所盛資料按照桶號由小到大(桶中由頂至底)依次重新收集串起來,得到如下仍然無序的資料序列:

81  22  73  93  43  14  55  65  28  39

接著,再進行一次分配,這次根據十位數值來分配(原理同上),分配結果(邏輯想象)如下圖所示:

分配結束後。接下來再將所有桶中所盛的資料(原理同上)依次重新收集串接起來,得到如下的資料序列:

14  22  28  39  43  55  65  73  81  93

觀察可以看到,此時原無序資料序列已經排序完畢。如果排序的資料序列有三位數以上的資料,則重複進行以上的動作直至最高位數為止。

那麼,到這裡為止,你覺得你是不是一個細心的人?不要不假思索的回答我。不論回答什麼樣的問題,都要做到心比頭快,頭比嘴快。

仔細看看你對整個排序的過程中還有哪些疑惑?真看不到?覺得我做得很好?抑或前面沒看懂?

如果你看到這裡真心沒有意識到或發現這個問題,那我告訴你:悄悄去找個牆角蹲下用小拇指畫圈圈(好好反省反省)。

追問:觀察原無序資料序列中73   93   43 三個資料的順序,在經過第一次(按照個位數值,它們三者應該是在同一個桶中)分配之後,

在桶中順序由底至頂應該為73  93  43(即就是裝的遲的在最上面,對應我們上面的邏輯想象應該是43  93  73),對吧?這個應該可以想明白吧?理論上應該是這樣的。

但是,但是,但是分配後很明顯在3號桶中三者的順序剛好相反。這點難道你沒有發現嗎?或者是發現了覺得不屑談及(算我貽笑大方)?

其實這個也正是基數排序穩定性的原因(分配時由末位向首位進行),請看下文的詳細分析。

再思考一個問題:既然我們可以從最低位到最高位進行如此的分配收集,那麼是否可以由最高位到最低位依次操作呢? 答案是完全可以的。

基於兩種不同的排序順序,我們將基數排序分為LSD(Least significant digital)或MSD(Most significant digital),

LSD的排序方式由數值的最右邊(低位)開始,而MSD則相反,由數值的最左邊(高位)開始。

注意一點:LSD的基數排序適用於位數少的數列,如果位數多的話,使用MSD的效率會比較好。

MSD的方式與LSD相反,是由高位數為基底開始進行分配,但在分配之後並不馬上合併回一個陣列中,而是在每個“桶子”中建立“子桶”,將每個桶子中的數值按照下一數位的值分配到“子桶”中。

在進行完最低位數的分配後再合併回單一的陣列中。

(2)我們把撲克牌的排序看成由花色和麵值兩個資料項組成的主關鍵字排序。

要求如下:

花色順序:梅花<方塊<紅心<黑桃

面值順序:2<3<4<...<10<J<Q<K<A

那麼,若要將一副撲克牌排成下列次序:

梅花2,...,梅花A,方塊2,...,方塊A,紅心2,...,紅心A,黑桃2,...,黑桃A。

有兩種排序方法:

<1>先按花色分成四堆,把各堆收集起來;然後對每堆按面值由小到大排列,再按花色從小到大按堆收疊起來。----稱為"最高位優先"(MSD)法。

<2>先按面值由小到大排列成13堆,然後從小到大收集起來;再按花色不同分成四堆,最後順序收集起來。----稱為"最低位優先"(LSD)法。

【2】程式碼實現

(1)MSD法實現

最高位優先法通常是一個遞迴的過程:

<1>先根據最高位關鍵碼K1排序,得到若干物件組,物件組中每個物件都有相同關鍵碼K1。

<2>再分別對每組中物件根據關鍵碼K2進行排序,按K2值的不同,再分成若干個更小的子組,每個子組中的物件具有相同的K1和K2值。

<3>依此重複,直到對關鍵碼Kd完成排序為止。

<4> 最後,把所有子組中的物件依次連線起來,就得到一個有序的物件序列。

示例程式碼如下:

#include<iostream>
#include<malloc.h>
using namespace std;

int getdigit(int x,int d)  
{   
    int a[] = {1, 1, 10};     //因為待排資料最大資料也只是兩位數,所以在此只需要到十位就滿足
    return ((x / a[d]) % 10);    //確定桶號
}  

void  PrintArr(int ar[],int n)
{
    for(int i = 0; i < n; ++i)
        cout<<ar[i]<<" ";
    cout<<endl;
}

void msdradix_sort(int arr[],int begin,int end,int d)  
{     
    const int radix = 10;   
    int count[radix], i, j; 
    //置空
    for(i = 0; i < radix; ++i)   
    {
        count[i] = 0;   
    }
    //分配桶儲存空間
    int *bucket = (int *) malloc((end-begin+1) * sizeof(int));    
    //統計各桶需要裝的元素的個數  
    for(i = begin;i <= end; ++i)   
    {
        count[getdigit(arr[i], d)]++;   
    }
    //求出桶的邊界索引,count[i]值為第i個桶的右邊界索引+1
    for(i = 1; i < radix; ++i)   
    {
        count[i] = count[i] + count[i-1];    
    }
    //這裡要從右向左掃描,保證排序穩定性 
    for(i = end;i >= begin; --i)          
    {    
        j = getdigit(arr[i], d);      //求出關鍵碼的第d位的數字, 例如:576的第3位是5   
        bucket[count[j]-1] = arr[i];   //放入對應的桶中,count[j]-1是第j個桶的右邊界索引   
        --count[j];                    //第j個桶放下一個元素的位置(右邊界索引+1)   
    }   
    //注意:此時count[i]為第i個桶左邊界    
    //從各個桶中收集資料  
    for(i = begin, j = 0;i <= end; ++i, ++j)  
    {
        arr[i] = bucket[j]; 
    }       
    //釋放儲存空間
    free(bucket);   
    //對各桶中資料進行再排序
    for(i = 0;i < radix; i++)  
    {   
        int p1 = begin + count[i];         //第i個桶的左邊界   
        int p2 = begin + count[i+1]-1;     //第i個桶的右邊界   
        if(p1 < p2 && d > 1)  
        {
            msdradix_sort(arr, p1, p2, d-1);  //對第i個桶遞迴呼叫,進行基數排序,數位降 1    
        }
    }  
} 

void  main()
{
    int  ar[] = {12, 14, 54, 5, 6, 3, 9, 8, 47, 89};
    int len = sizeof(ar)/sizeof(int);
    cout<<"排序前資料如下:"<<endl;
    PrintArr(ar, len);
    msdradix_sort(ar, 0, len-1, 2);
    cout<<"排序後結果如下:"<<endl;
    PrintArr(ar, len);
} 
/*
排序前資料如下:
12 14 54 5 6 3 9 8 47 89
排序後結果如下:
3 5 6 8 9 12 14 47 54 89
 */

(2)LSD法實現

最低位優先法首先依據最低位關鍵碼Kd對所有物件進行一趟排序,

再依據次低位關鍵碼Kd-1對上一趟排序的結果再排序,

依次重複,直到依據關鍵碼K1最後一趟排序完成,就可以得到一個有序的序列。

使用這種排序方法對每一個關鍵碼進行排序時,不需要再分組,而是整個物件組。

示例程式碼如下:

#include<iostream>
#include<malloc.h>
using namespace std;

#define   MAXSIZE   10000

int getdigit(int x,int d)  
{   
    int a[] = {1, 1, 10, 100};   //最大三位數,所以這裡只要百位就滿足了。
    return (x/a[d]) % 10;  
}  
void  PrintArr(int ar[],int n)
{
    for(int i = 0;i < n; ++i)
    {
        cout<<ar[i]<<" ";
    }
    cout<<endl;
}  
void lsdradix_sort(int arr[],int begin,int end,int d)  
{    
    const int radix = 10;   
    int count[radix], i, j; 

    int *bucket = (int*)malloc((end-begin+1)*sizeof(int));  //所有桶的空間開闢   
   
    //按照分配標準依次進行排序過程
    for(int k = 1; k <= d; ++k)  
    {  
        //置空
        for(i = 0; i < radix; i++)  
        {
            count[i] = 0;        
        }               
        //統計各個桶中所盛資料個數
        for(i = begin; i <= end; i++) 
        {
           count[getdigit(arr[i], k)]++;
        }
        //count[i]表示第i個桶的右邊界索引
        for(i = 1; i < radix; i++) 
        {
            count[i] = count[i] + count[i-1];
        }
        //把資料依次裝入桶(注意裝入時候的分配技巧)
        for(i = end;i >= begin; --i)        //這裡要從右向左掃描,保證排序穩定性   
        {    
            j = getdigit(arr[i], k);        //求出關鍵碼的第k位的數字, 例如:576的第3位是5   
            bucket[count[j]-1] = arr[i]; //放入對應的桶中,count[j]-1是第j個桶的右邊界索引 
            --count[j];               //對應桶的裝入資料索引減一  
        } 

        //注意:此時count[i]為第i個桶左邊界  
        
        //從各個桶中收集資料
        for(i = begin,j = 0; i <= end; ++i, ++j)  
        {
            arr[i] = bucket[j];    
        }        
    }     
    free(bucket);   
}  

void  main()
{
    int  br[10] = {20, 80, 90, 589, 998, 965, 852, 123, 456, 789};
    cout<<"原資料如下:"<<endl;
    PrintArr(br,10);
    lsdradix_sort(br, 0, 9, 3);
    cout<<"排序後資料如下:"<<endl;
    PrintArr(br, 10);
}
/*
原資料如下:
20 80 90 589 998 965 852 123 456 789
排序後資料如下:
20 80 90 123 456 589 789 852 965 998
*/

注意:以上兩種方法我們均用陣列模擬桶,關於陣列模擬桶詳細講解請參考隨筆《桶排序

【3】基數排序穩定性分析

基數排序是穩定性排序演算法,那麼,到底如何理解它所謂的穩定特性呢?

比如:我們有如下欲排資料序列:

下面選擇LSD邏輯演示

第一次按個位數值分配,結果如下圖所示:


然後收集資料結果如下:

第二次按十位數值分配,結果如下圖所示:

然後收集資料結果如下:

相關文章