基本思想
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。
即不需要直接对元素进行相互比较,也不需要将元素相互交换,你需要做的就是对元素进行“分配”。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位(或最高位)开始,依次进行一次分配。这样从最低位(或最高位)一直到最高(或最低)位的分配完成以后, 数列就变成一个有序序列。
因此基数排序有两种实现方法。
实现方法
最高位优先(Most Significant Digit First)
最高位优先(Most Significant Digit first)法,简称MSD法:先按k1(即数字最高位)排序分组,同一组中记录,关键码k1(最高位)相等,再对各组按k2(即次高位)排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd(即最低位)对各子组排序后。再将各组连接起来,便得到一个有序序列。
最低位优先(Least Significant Digit First)
最低位优先(Least Significant Digit first)法,简称LSD法:先从kd(即数字最低位)开始排序,再对kd-1(次低位)进行排序,依次重复,直到对k1(最高位)排序后便得到一个有序序列。
LSD代码实现(C++)
//辅助函数:得到给定数字的位数
int getMaxDigit(int x)
{
int digit = 1; //默认位数位1位,及0-9
int radix = 10; //默认基数位10
while (x >= radix)
{
//大于10说明至少为2位,后面大于100,1000等同理
digit++; //位数加一
radix *= 10; //进位
}
return digit;
}
//基数排序
void radixSort(vector<int>& v)
{
//先得到数组中元素的最大位数, 即需要排序的次数
int time = 0;
for (int x : v)
{
time = max(getMaxDigit(x), time); //getMaxDigit(x)为辅助函数,用来得到x的位数
}
cout << "有" << time << "轮排序\n";
cout << endl;
//排序过程中需要用到的临时排序空间,这里使用一个临时的10个队列大小的数组
vector<queue<int>> tmp(10, queue<int>());
//这里以最低位优先LSD(Least Significant Digit First)为例
int radix = 1; //即先从低位(个位)开始排序
for (int i = 0; i < time; i++)
{
//分配操作:对低位排序
for (int j = 0; j < v.size(); j++)
{
//通过v[j] / radix % 10得到当前元素v[j]最低位,并将当前元素存储到对应下标的队列中。例21 / 1 % 10 = 1
tmp[v[j] / radix % 10].push(v[j]);
}
//收集操作:将排序结果放入原数组
int k = 0; //重置原数组索引
for (int j = 0; j < tmp.size(); j++)
{
while (!tmp[j].empty())
{
//对应队列不为空说明该队列中有排序的元素
v[k++] = tmp[j].front();
tmp[j].pop();
}
}
//测试每轮排序的结果
cout << "******************************************\n";
cout << "第" << i + 1 << "轮排序\n";
for (int a : v)
{
cout << a << " ";
}
cout << endl;
cout << "******************************************\n";
cout << endl;
//修改radix,进位对高位排序
radix *= 10;
}
}
排序过程
将数组 {53, 22,31,95,53,14,117} 以LSD法排序,注意黑色加粗的53用来判断算法稳定性。
首先得到数组中最大元素的位数3,因此需要进行三轮排序。同时我们以一个临时的10队列大小的数组(vector<queue>)来进行每轮排序。
第一轮排序:对个位分类
|下标| 对应队列 |
| 0 | null |
| 1 | 31 |
| 2 | 22 |
| 3 | 53 53 |
| 4 | 14 |
| 5 | 95 |
| 6 | null |
| 7 | 117 |
| 8 | null |
| 9 | null |
得到第一轮排序结果[31, 22, 53, 53, 14, 95, 117],可以发现排序过程中并没有比较操作。
第二轮排序:对十位分类
|下标| 对应队列 |
| 0 | null |
| 1 | 14 117 |
| 2 | 22 |
| 3 | 31 |
| 4 | null |
| 5 | 53 53 |
| 6 | null |
| 7 | null |
| 8 | null |
| 9 | 95 |
得到第二轮排序结果[14, 117, 22, 31, 53, 53, 95]
第三轮排序:对百位分类
|下标| 对应队列 |
| 0 | 14 22 31 53 53 95 |
| 1 | 117 |
| 2 | null |
| 3 | null |
| 4 | null |
| 5 | null |
| 6 | null |
| 7 | null |
| 8 | null |
| 9 | null |
得到第三轮排序结果[14, 22, 31, 53, 53, 95, 117],至此排序结束。
时间复杂度
在不考虑队列的push和pop操作开销的情况下,上面代码的时间复杂度位O(n + time*(n + radixn)),radix为关键码的取值范围,在本例中是10,即O(time(n + 10*n)),化简为O(time * n)。
time为最外层for循环的排序轮数,第一个n为取给定数组最大元素位数的时间开销,第二个n 为分配操作的时间开销,radix = 10, 第三个n 为收集操作的时间开销。
空间复杂度
主要的空间开销在于vector<queue> tmp(radix, queue()),radix为关键码的取值范围(本例中为10),第二维度的队列的取值范围为[0, n - 1], 所以空间复杂度为O(radix * n),明显还有优化空间。
算法稳定性
排序过程中黑色加粗的53始终位于未加粗的53前面,相对位置没有发生改变,所以基数排序是稳定的排序算法。