1. 基本思想
基數排序(radix sort
)可以看作桶排序的擴展,它是一種多關鍵字排序算法。 如果記錄安照多個關鍵字排序,則依次按照這些關鍵字進行排序。例如撲克牌排序,撲克牌由數字面直和花色兩個關鍵字組成,可以先按照面值(2, 3,.,. 10, J,Q, K, A
)排序,再按照花色排序。如果記錄按照一個數值型的關鍵字排序,可以把該關鍵字看作是由 d
位組成的多關鍵字排序,每一位的值取值範圍爲 [0,r)
, 其中 r
稱爲基數。例如,十進制數 268 由 3 位數組成,每一位的取值範圍爲 [0,10)
, 十進制數的基數 r
爲 10,同樣,二進制數的基數爲 2,英文字母的基數爲 26。 以下以十進制數的基數排序爲例。
基數排序與桶排序很類似,主要算法步驟如下:
- 求出待排序序列中最大關鍵字的位數
d
,然後從低位到高位進行基數排序 - 按個位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來
- 按十位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來
- 依次下去, 直到
d
位處理完畢,得到一個有序的序列
舉個例子:假如有 10 個學生的成績,(68,75,54,70,83,48,80,12,75*,92)
,對該成績序列進行桶排序。需要進行以下步驟:
- 確定關鍵字
待排序序列中最大關鍵字 92 爲兩位數,只需要兩趟基數排序即可 - 分配
首先按照個位數,劃分爲 10 個桶 (0~9
),將學生成績依次放入桶中,如下圖所示:
- 收集
將每個桶內的記錄依次收集起來,得到一個序列(70,80,12,92,83,54,75,75*,68,48)
- 分配
再按照十位數,劃分爲10個桶(0~9
), 將學生成績依次放入桶中,如下圖所示:
- 收集
將每個桶內的記錄依次收集起來,得到一個序列(12,48,54,68,70,75,75*,80,83,92
)。待排序數據都是兩位數,只有兩個關鍵字,排序完畢,得到一個有序序列。
在此着重強調一個概念,依次,分配和收集時爲什麼要“依次”放入和收集?如果不是“依次”會怎麼樣?
回答這個問題,依舊舉個例子:例如對(82,62,65,85
)進行基數排序,首先按照個位劃分:
收集桶中的數據,(62,82,85,65
),再按照十位劃分到 6 號和 8 號桶中:
收集桶中的數據,(65,62,85,82
),排序結束並不是一個有序的序列,爲什麼?
- 以 62、65 爲例,首先按照個位劃分,62 在 2 號桶,65 在 5 號桶,2 號桶在 5 號桶的前面,那麼相同十位的情況下,桶間是有序的,即2 號桶的元素肯定是小於 5 號桶的,那麼收集的時候就需要依次進行收集,不能隨意更改收集的方式。
如果不是按順序依次進行分配和收集則無法實現排序結果的正確性。那麼如何保證依次分配和收集呢?
- 一個非常簡單的方法就是隊列,先進先出,依次進行。因此可以採用隊列保持桶中數據的進出順序,保證排序結果的正確性。也就是說,每一個桶內使用一個隊列存儲數據,可以使用順序隊列或鏈式隊列。
桶中的多個數據元素可以採用二維數組、鏈式存儲的方式都可以,主要保證“依次”屬性即可。下面着重學習一種一位數組的方式進行處理的做法及算法思想:
針對序列:(68,75,54,70,83,48,80,12,75*,92)
進行基數排序,首先按照個位數,,劃分 10 個桶(0~9
),將學生成績依次放入桶中,個位是 0 的放入 0 號桶,個位是 2 的放入 2 號桶,等等。如圖所示:
重點:建立輔助數組爲–計數器數組,如圖所示。個位爲0的有兩個,count[0]=2
:
將計數器數組累加,從下標 1 開始,累加前一項,count[j] += count[j - 1]
,如圖所示:
累加的效果相當於分配存儲區間,例如,count[8]=10
, 那麼 8 號桶的兩個數分配存儲空間下標爲 9,8; count[5]=8
, 那麼 5 號桶的兩個數分配存儲空間下標爲7,6,因爲下標從 0 開始。如圖所示:
利用 count[]
數組,將桶中的數據收集到輔助數組 temp
中。序列 從後向前 處理,(68,75,54,70,83,48,80,12,75* 92
),相當於一種 映射關係:
- 92 在 2 號桶,
count[2]=4
,--count[2]=3
, 將 92 存入temp[3]
,依次處理完畢後有:
將 temp[]
數組中的數據,按照十位數,劃分爲 10 個桶(0~9
)
計數器數組,如圖所示。十位爲 7 的有 3 個,count[7]=3
將計數器數組累加,從下標 1 開始,累加前一項,count[j]+=countfj-1]
, 如圖所示。
利用 count[]
數組,將桶中的數據收集到輔助數組temp
中。序列從後向前處理,(70,80,12,92,83,54,75,75*,68,48
)
- 48 在 4 號桶,
count[4]=2
,--count[4]=1
, 將 48 存入temp[1]
將排好序的輔助數組 temp[]
放回原數組即可。排序結果如圖所示。
2. 代碼實現
2.1 動態二維數組實現
// 基數排序(遞增)
int Maxbit(int array[], int size) { // 求待排序序列最大元素位數
int maxvalue = array[0], digits = 0; // 初始化最大元素爲array[0],最大位數爲0
for (int i = 1; i < size; i++) { // 找到序列中最大元素
if (array[i] > maxvalue)
maxvalue = array[i];
}
while (maxvalue != 0) { // 分解得到最大元素的位數
digits++;
maxvalue /= 10;
}
return digits;
}
int Bitnumber(int x, int bit) { // 求x第bit位上的數字,例如238第2位上的數字爲3
int temp = 1;
for (int i = 1; i < bit; i++) {
temp *= 10;
}
return (x / temp) % 10;
}
// 基數排序(遞增)
void RadixSort(int array[], int size) {
int i, j, k, bit, maxbit;
maxbit = Maxbit(array, size); // 求最大元素位數
cout << "最大元素位數爲:" << maxbit << "位 " << endl;
int **B = new int *[10]; // 分配二維動態數組
for (i = 0; i < 10; i++)
B[i] = new int[size + 1]; // 每個桶都是size+1個空間,其中每個桶的第一個位置即B[0]第0位存放元素個數
for (i = 0; i < 10; i++)
B[i][0] = 0; //統計第i個桶的元素個數
// 從個位到高位,對不同的位數進行桶排序
for (bit = 1; bit <= maxbit; bit++) {
for (j = 0; j < size; j++) { // 分配
int num = Bitnumber(array[j], bit); // 取array[j]第bit位上的數字
int index = ++B[num][0];
B[num][index] = array[j];
}
for (i = 0, j = 0; i < 10; i++) { // 收集
for (k = 1; k <= B[i][0]; k++)
array[j++] = B[i][k];
B[i][0] = 0; // 收集後元素個數置零
}
}
for (int i = 0; i < 10; i++)
delete[]B[i];
delete B;
}
測試數據:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
2.2 一維數組實現
// 基數排序(遞增)
// 一維數組實現
const int maxn = 1000;
int a[maxn], size;
int maxbit(int array[], int size) { //輔助函數,求數據的最大位數
int d = 1;//統計最大的位數
int p = 10;
for (int i = 0; i < size; ++i) {
while (array[i] >= p) {
p *= 10;
++d;
}
}
return d;
}
// 基數排序(遞增)
// 一維數組實現
void radixsort(int array[], int size) {
int d = maxbit(array, size); // 求最大位數
int *tmp = new int[size]; // 輔助數組
int *count = new int[10]; // 計數器
int i, j, k;
int radix = 1;
for (i = 1; i <= d; i++) { // 進行d次排序
for (j = 0; j < 10; ++j) {
count[j] = 0; // 每次分配前清空計數器
}
for (j = 0; j < size; ++j) {
k = (array[j] / radix) % 10; // 取出個位數,然後是十位數,...
count[k]++; // 統計每個桶中的記錄數
}
for (j = 1; j < 10; ++j) {
count[j] += count[j - 1]; // 將tmp中的位置依次分配給每個桶
}
for (j = size - 1; j >= 0; --j) { //將所有桶中記錄依次收集到tmp中
k = (array[j] / radix) % 10;
tmp[--count[k]] = array[j];
}
for (j = 0; j < size; j++) { // 將臨時數組的內容複製到array中
array[j] = tmp[j];
}
cout << "第" << i << "次排序結果:" << endl;
for (int i = 0; i < size; ++i)
cout << array[i] << " ";
cout << endl;
radix = radix * 10;
}
delete[]tmp;
delete[]count;
}
3. 性能分析
3.1 鏈式隊列性能分析
時間複雜度
基數排序需要進行 d
趟排序,每一趟排序包含分配和收集兩個操作,分配需要時間,收集操作如果使用順序隊列也需要時間,如果使用鏈式隊列則只需要
將 r
個鏈隊首展相連即可,需要時間,總的時間複雜度爲。
空間複雜度
- 如果使用順序隊列,需要
r
個大小爲n
的隊列,空間複雜度爲。如果使用鏈式隊列,則需要額外的指針域,空間複雜度爲。
排序穩定性
- 穩定,基數排序時按關鍵字出現的順序依次進行的
3.2 一維數組實現
時間複雜度
基數排序需要進行 d
趟排序,每一趟排序包含分配和收集兩個操作,分配需要時間,收集操作使用一維數組需要時間,總的時間複雜度爲。
空間複雜度
- 使用計數數組
count
的大小爲基數r
,輔助數組temp
的大小爲n
,空間複雜度爲 。
排序穩定性
- 穩定,基數排序時按關鍵字出現的順序依次進行的