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
趟排序,每一趟排序包含分配和收集两个操作,分配需要
O
(
n
)
O(n)
O(n)时间,收集操作如果使用顺序队列也需要
O
(
n
)
O(n)
O(n)时间,如果使用链式队列则只需要
将 r
个链队首展相连即可,需要
O
(
r
)
O(r)
O(r)时间,总的时间复杂度为
O
(
d
(
n
+
r
)
)
O(d(n+r))
O(d(n+r))。
空间复杂度
- 如果使用顺序队列,需要
r
个大小为n
的队列,空间复杂度为 O ( m ) O(m) O(m)。如果使用链式队列,则需要额外的指针域,空间复杂度为 O ( n + r ) O(n+r) O(n+r)。
排序稳定性
- 稳定,基数排序时按关键字出现的顺序依次进行的
3.2 一维数组实现
时间复杂度
基数排序需要进行 d
趟排序,每一趟排序包含分配和收集两个操作,分配需要
O
(
n
)
O(n)
O(n)时间,收集操作使用一维数组需要
O
(
n
)
O(n)
O(n)时间,总的时间复杂度为
O
(
d
×
n
)
O(d{\times}n)
O(d×n)。
空间复杂度
- 使用计数数组
count
的大小为基数r
,辅助数组temp
的大小为n
,空间复杂度为 O ( n + r ) O(n+r) O(n+r)。
排序稳定性
- 稳定,基数排序时按关键字出现的顺序依次进行的