內部排序之基數排序
技術標籤:C++
1. MSD(Most Significant Digit First)排序準則
對記錄按關鍵字組(K0, K1, K2, … , Kd-1)進行排序時,先按K0排序,使K0相同的記錄排列在一起,然後對K0相同的每個子序列按K1排序,使K0相同的每個子序列進一步細分為K1相同的若干個更小的子序列,如此重複,直至Kd-1排序結束。基於MSD準則的排序等效於按照關鍵字組比較大小的定義直接對關鍵字組比較大小來實現排序,即只需要定義如下關鍵字組比較大小的函式即可實現MSD準則排序。
一個例子(比較時間的大小)
定義時間的結構體:
typedef struct
{
int hour,
min,
sec;
} Time;
依據MSD排序準則對時間進行比較,預設採用24小時時間制,一天從00:00:00開始,從23:59:59結束。越“晚”的時間越“大”,越“早”的時間越“小”。例如下午三點三分三秒是要大於凌晨三點三份三秒的,即15:03:03>03:03:03。比較原理就是MSD準則,從最高位開始,依次向最低位比較。
原始碼
#include <iostream>
using namespace std;
typedef struct
{
int hour,
min,
sec;
} Time;
int compareTime(Time &a, Time &b);
int main()
{
Time a = {5, 4, 4}, b = {4, 4, 4}; //初始化,也可鍵入
int result = compareTime(a, b);
cout << result << endl; //輸出結果
system("pause");
return 0;
}
int compareTime(Time &a, Time &b)
{ //a=b返回0,a>b返回1,a<b返回-1
//兩個關鍵字組,d=3
int aK[3] = {a.hour, a.min, a.sec}, bK[3] = {b.hour, b.min, b.sec};
int i;
//依據MSD排序準則,從最高位開始比較
for (i = 0; i < 3; i++)
if (aK[i] != bK[i])
break;
//i>=3說明每一位都相同,"正常結束"迴圈
if (i >= 3)
return 0;
//"異常結束"迴圈再比較使得迴圈結束的這一位的大小來判斷a,b的大小
if (aK[i] > bK[i])
return 1;
else
return -1;
}
2. LSD(Least Significant Digit First)排序準則
各關鍵字位按由低位到高位的次序排序。這需要通過“分配”和“收集”兩種操作來實現。對於d元關鍵字組,需要進行d趟分配和收集操作。LSD準則實現的排序常稱為基數排序。基數排序非常適合採用鏈式儲存結構。為方便操作,每趟分配得到的每個分組用帶頭、尾指標的鏈式佇列來實現儲存。分配時,序列中的每個結點按連線次序依次摘下並連線到分組關鍵字相同的分組(佇列)的隊尾(這樣可以確保同一佇列中記錄的連線先後次序與它們在原序列中的先後次序相同)。當序列中的所有結點摘完後,則一趟分配操作結束。收集時,只需將所有分組(佇列)按分組關鍵字有序的次序首尾相連即生成新的序列。其實我們可以發現,基數排序是箱子排序的"拓展",理解箱子排序有助於我們理解基數排序。
2.1基數排序的手工求法
以下面這個三位十進位制序列為例子,求其基數排序的結果:{278, 109, 063, 930, 589, 184, 505, 269, 008, 083}
第一趟:按照“個位”進行分配和收集
第二趟:按照“十位”進行分配和收集
注意:第二趟的輸入為第一趟收集的結果
第三趟:按照“百位”進行分配和收集
最終結果為:{008,063,083,109,184,269,278,505,589,930}
2.2基數排序的演算法實現
以三位四進位制數的演算法實現為例。首先我們定義結構體:
typedef struct
{ //關鍵字序列以及指標域(靜態連結串列)
int K[3];
int next; //值為其下一個結點的元素下標
} elemTp;
其中整型陣列K[d],(在這個例子中d=3),代表每一個元素的“位數”,對應三位四進位制中的三位;整數next代表指標,它的值是該結點的下一個結點的下標。設每一個關鍵字的每一位的可能的取值為整型陣列rd[]
,我們可以很容易的知道三位四進位制中的四進位制是什麼意思,所以陣列rd中的值為0,1,2,3,故我們用其來初始化陣列rd並保持其不變(定義在所有函式之外)int rd[4] = {0, 1, 2, 3};
依據箱子排序的原理,顯然我們需要知道每一個關鍵字需要插入的佇列序號,於是我們定義了一個函式sp
int sp(int K)
{ //定義sp函式:返回待排序關鍵字應該插入的佇列數
int j;
for (j = 0; j < 4; j++)
if (K == rd[j])
break;
return j;
}
然後我們就可以開始進行排序了,其基本思想還是“分配”和“收集”,通過每一個關鍵字的每一位與rd陣列進行匹配,尋找到其應該插入的佇列序號,然後收集起來;再分配,再收集;直至關鍵字序列有序。
原始碼
//三位四進位制基數排序
#include <iostream>
using namespace std;
int rd[4] = {0, 1, 2, 3};
typedef struct
{ //關鍵字序列以及指標域(靜態連結串列)
int K[3];
int next; //值為其下一個結點的元素下標
} elemTp;
int radixSort(elemTp R[], int n);
int main()
{
elemTp R[6] = {{1, 2, 3}, {1, 2, 2}, {1, 2, 1}, {1, 1, 3}, {1, 1, 2}, {1, 1, 1}};
for (int h = radixSort(R, 6); h != -1; h = R[h].next)
{
for (int i = 0; i < 3; i++)
cout << R[h].K[i];
cout << " ";
}
cout << endl;
system("pause");
return 0;
}
int sp(int K)
{ //定義sp函式:返回待排序關鍵字應該插入的佇列數
int j;
for (j = 0; j < 4; j++)
if (K == rd[j])
break;
return j;
}
int radixSort(elemTp R[], int n)
{ //基數排序,演算法結束後返回靜態連結串列頭結點下標
//各結點next域初始化
int i;
//n個結點,共有n-1個指標
for (i = 0; i < n - 1; i++)
R[i].next = i + 1;
int h = 0, p = 0;
R[n - 1].next = -1; //h為頭指標, -1表示連結串列結束
//依據LSD排序準則
for (i = 3 - 1; i >= 0; i--)
{ //第i趟分配
int *head = new int[4], *rear = new int[4];
int k;
//初始化隊頭指標
for (k = 0; k < 4; k++)
head[k] = -1;
while (h != -1)
{
k = sp(R[h].K[i]); //R[h]應插入到第k個佇列中
if (head[k] == -1)
head[k] = h;
else
R[rear[k]].next = h;
rear[k] = h;
h = R[h].next;
}
//第i趟收集
//找到第一個非空佇列,h為收集後的連結串列頭,p跟蹤當前連結串列尾結點
for (k = 0; k < 4; k++)
if (head[k] != -1)
break;
h = head[k];
p = rear[k];
//連結後續的非空佇列
while (++k < 4)
if (head[k] != -1)
{
R[p].next = head[k];
p = rear[k];
}
//使連結串列結束
R[p].next = -1;
//釋放佇列頭、尾指標記憶體
delete[] head;
delete[] rear;
} //end for
//返回連結串列頭結點下標
return h;
}
2.3基數排序的演算法複雜度分析
(1) 時間複雜度
第i趟分配:迴圈n次(n為記錄總數); 第i趟收集:迴圈rd(i)次;共d趟分配和收集,故總迴圈次數為表示簡單,記rd為rd(i)的最大值,於是T(n)=O(d(n+rd))
(2) 空間複雜度
輔助空間:O(rd)
參考資料
西南交通大學資訊科學與技術學院軟體工程系‐趙巨集宇 資料結構A教學PPT 第9章