一、前言
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
比较型排序:常见的快速排序,归并排序,冒泡排序……等等,都是基于比较的排序算法。
比较型排序算法时间复杂度下界为O(N*log2N) ,
而非比较型排序算法有:计数排序,桶排序,基数排序等;
其中,计数排序,桶排序的时间复杂度分别为O(n+m)和O(n),线性的时间复杂度。
计数排序和桶排序这么快,为什么STL, JDK等没有采用呢?
因为适用场景比较窄。
要使用这两个算法,需满足如下条件:
1、排序项是数值:这个就很伤了,比如不能用来排序字符串了,更不要说各种复杂对象了;
2、范围比较小:如果数值范围比较大,需要的计数器或者桶就会很多,空间复杂度上无法承受。
比如阿里面试题有一道是给2万名员工按年龄排序,就可以用计数排序或桶排序了,
因为年龄是整数,而且范围小,比如用桶排序,顶多100个桶就够了。
而基数排序,正是解决计数排序和桶排序数值范围局限性的良方。
二、原理
基数排序是这样实现的:
将所有待比较数值统一为同样的数字长度,数字较短的数前面补零。
然后,从最低位开始,依次进行一次排序。
这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
以上是以10为“基数”的过程演示。
整个过程经过三轮排序,先个位,再十位,然后是百位; 三轮排序完成后,整个数列就有序了。
那这三轮排序都是怎么操作的呢?计数排序或桶排序都可以。
所以基数排序和计数排序、桶排序的关系是一种拓展的关系。
其背后时一种时间和空间的交换。
比如说这里如果用1000个桶,那么只用一轮排序就好了(这就退化到计数排序了)。
这里先不讨论是否划算(用10为基通常是不划算的), 先分析思想原理。
如果说基数排序的核心奥义是“按位切割,分别比较”的话,那么
计数排序就是:先统计,后索引;
桶排序则是:先分配,后收集。
什么时候用计数排序,什么时候用基数排序?
我的理解是,数组用计数排序,链表则用桶排序(收集的时候执行各个桶的元素首位相接即可)。
下面是整个基数排序的过程(用上面的数据为例):
第一轮:
0: 170, 90
1:
2: 802, 2
3:
4: 24
5: 45,75
6: 66
7:
8:
9:
第二轮:
0: 802, 2
1:
2: 24
3:
4: 45
5:
6: 66
7: 170, 75
8:
9: 90
第三轮:
0: 2, 24, 45, 66, 75, 90
1: 170
2:
3:
4:
5:
6:
7:
8: 802
9:
最终,顺序为: 2, 24, 45, 66, 75, 90,170, 802
值得一提的是,实现这个过程的一个必要条件是:计数排序和桶排序是稳定排序。
三、实现
C++版本
template <class T>
void countSort(T *src, T *des, int n, int shift) {
// 初始化统计变量为0
int cnt[256] = {
0 };
// 获取元素的value,通过位移和掩码获取[shift,shift+8]的bit, 索引到对应的统计变量(cnt)
// 统计散落到各cnt的元素的数量
for (int i = 0; i < n; i++) {
T value = src[i];
cnt[(value >> shift) & 0xFF]++;
}
// 根据cnt统计的元素个数,计算每个一组元素的末端位置
for (int i = 1; i < 256; i++) {
cnt[i] += cnt[i - 1];
}
// 再次根据元素的value索引到对应的统计变量(cnt)
// 结合上一步前面计算的位置,将各个元素放到对应的位置(另一个素组)
for (int i = n - 1; i >= 0; i--) {
T value = src[i];
des[--cnt[(value >> shift) & 0xFF]] = src[i];
}
}
template <class T>
void rsort(T *src, int n) {
T* des = new T[n];
int size = sizeof(T);
// 以256为基,则需要每次取元素的8bit进行计数排序,从低位到高位
// bit为一个字节,则元素大小有多少个字节就需要进行多少次计数排序
for (int i = 0; i < size; i++) {
if (i % 2 == 0)
countSort(src, des, n