第一类:最低位优先法,简称LSD法:先从最低位开始排序,再对次低位排序,直到对最高位排序后得到一个有序序列。本文将介绍这种方法。
第二类:最高位优先法,简称MSD法:先从最高位开始排序,再逐个对各分组内部按次高位进行子排序,循环直到最低位。 这种方法的优势是可以处理长度不一致的数据,也就是适合排序字符串;缺点就是不好实现。这种方法本文不做介绍。
下图是 最容易理解的 以 10 为基数的基数排序(LSD法)的图解:
那么要解决的最大的问题是: 按照 各个数位 进行排序是怎么实现的!?
解决问题: 通过计数可以知道在某个数前面应该有多少个数!
① 判断每个数的哪一位是什么:( 数值 / 基数 ^k) % 基数; 个位k取0,十位k取1,百位k取2。。。;比如 137 的十位数是 (137 / 10) % 10 = 3 。
② 需要建立一个数组来记录 某一位数为几的 数的个数,这里基数取10,任意一位可能的数值是 0-9 ,即建立一个大小为10的数组。
接下来,就要解决 待排数组中的数 与 记录数组中的数的 映射关系。
③ 遍历待排数组,元素的 正在排序的那一个 数位为几,则记录数组的 对应下标的数 +1; 比如先排的是 个位,现在遍历到了数字 32,个位数为2,则 记录数组[2] += 1。
④ 遍历记录数组,做 记录数组[i + 1] = 记录数组[ i ] + 记录数组[i + 1] 的操作。这么做就能知道待排数组中 排序位数 为几 的数,在排序后的数列中的哪个地方! 因为 记录数组[ i ]的数值 代表了 在i +1之前应该要排多少个数。
⑤ 再次遍历待排数组,注意:这回要 从尾部往头部 遍历!!!!只有这样才能保证排序的稳定性!(读者思考为什么?) 记录数组[ 数字 某位数的值 ] - 1, 即为 该数 在排序后数组中对应的位置,同时 记录数组对应处的值减去1。
基数排序 待排数组中的数 与 记录数组中的数的 映射关系 有些技巧,需要从代码中去理解,其核心就是:通过计数可以知道在某个数前面应该有多少个数。
按照上述,给出以 10 为基数的版本:
template
void radixSort(_T src[], _T tmp[], int len)
{
//首先,获取与最大数同位数的10的幂次方数 1,10,100,1000...
int maxRadix = 1;
for(int i = 0; i < len; i++)
{
if(src[i]/maxRadix > 9) maxRadix *= 10;
}
int cnt[10] = { 0 };
for(int radix = 1; radix <= maxRadix; radix *= 10)
{
//计数数组置零
for(int i = 0; i < 10; i++)
{
cnt[i] = 0;
}
//对应位 +1
for(int i = 0; i < len; i++)
{
cnt[(src[i]/radix)%10]++;
}
//换算成tmp的 [对应位+1]
for(int i = 1; i < len; i++)
{
cnt[i] += cnt[i - 1];
}
//src中的元素 装到tmp对应的位置
for(int i = len - 1; i >= 0; i--)
{
tmp[cnt[(src[i]/redix)%10] - 1] = src[i];
cnt[(src[i]/redix)%10]--;
}
//拷贝回src
for(int i = 0; i < len; i++)
{
src[i] = tmp[i];
}
}
}
直接跳到核心部分。以2的倍数为基数,可以将 ( 数值 / 基数 ^k) % 基数 的 除法运算变为右移运算,取模运算变为与运算!从而大大提高速度。
1、那基数的取值选多少呢?
① 常见的数据类型为 1字节(比如char),2字节(比如short), 4字节(int, float), 8字节(long long, double)。
*目前最大的整型数据类型为64位,所以暂时只能排序64位以内的数据。 虽然有 bitset类 可以作为 高于 64 位的 支持位操作的载体,但是 bitset类 的位操作效率还是远低于整型数据的。
② 取1个字节,即基数为 0xFF,int[0x100] 要1kb; 取2个字节,即基数为 0xFFFF,int[0x10000] 要256kb;如果取4个字节,int[0x100000000] 要16g的内存。。。目前是不可取的方案。所以基数选 0xFF 或 0xFFFF。
*基数取 0xFFFF 只有在数据量非常大时才会显示出优势(尤其是 对于 32位的数据,数据量更加要 非常非常非常大 的时候才会呈现出优势)。至于这个数据量得有多大才会有优势,因电脑配置而异。
2、基数排序适用于非整型数据的排序吗?
适用,不过得稍作修改。通过 计算机里头浮点数的储存方式 可以得出一个结论:浮点数 忽略符号位,剩下的位在解释为无符号整数编码后 所得到的无符号数 顺序不变! 于是我们可以得到一种排序思路:先带符号位排序,然后对负数部分特殊处理。
下面给出 适用于各种基础数据类型的 基数排序:
通过模板,一次实现实现:
基数排序应用于浮点数 float 和 double
基数排序应用于整数 short,int ,long 和 long long
基数排序应用于字符类型 char
/**
基数排序 64位及以下 数据通用
_THRESHOLD 为 基数从 0x100 转为 0x10000 的阈值; *8位,及 16位 数据不支持基数 0x10000,请勿修改其阈值,改了也没有
_THRESHOLD 因设备而异,需要试验
*/
template < size_t _N > struct judgeType { typedef void T1; };
template<> struct judgeType<1> { typedef __int8 T1; static const unsigned int _THRESHOLD = 4294967295;};
template<> struct judgeType<2> { typedef __int16 T1; static const unsigned int _THRESHOLD = 4294967295;};
template<> struct judgeType<4> { typedef __int32 T1; static const unsigned int _THRESHOLD = 4294967295;};
template<> struct judgeType<8> { typedef __int64 T1; static const unsigned int _THRESHOLD = 12000;};
//template<> struct judgeType<16> { typedef __int128 T1; static const unsigned int _THRESHOLD = 4294967295;};
template < bool IS_OVER_THRESHOLD > struct pre_rsort_{
template
static void rsort(_T *src, _T *tmp, int len, int bit)
{
using T = judgeType::T1;
constexpr int _RADIX = 256 * ((255 * IS_OVER_THRESHOLD) + 1) - 1;
int idx, sum, *cur;
_T* ptr;
int cnt[_RADIX + 1] = { 0 };
ptr = src;
idx = len;
do
{
cnt[((*(T *)ptr++) >> bit) & _RADIX]++;
} while (--idx != 0);
cur = cnt;
idx = _RADIX + 1;
sum = 0;
do
{
int c = *cur;
*cur++ = sum;
sum += c;
} while (--idx != 0);
ptr = src;
idx = len;
do
{
tmp[cnt[((*(T *)ptr++) >> bit) & _RADIX]++] = *ptr;
} while (--idx != 0);
}
};
template < size_t _N, bool IS_OVER_THRESHOLD > struct wrsort_ {
template
static void rrsort(_T *src, _T *tmp, int len)
{
wrsort_<( _N - 2 * (1 + IS_OVER_THRESHOLD)), IS_OVER_THRESHOLD>::rrsort(src, tmp, len);
pre_rsort_::rsort(src, tmp, len, 8 * (1 + IS_OVER_THRESHOLD) * (_N / (int)(1 + IS_OVER_THRESHOLD) - 2));
pre_rsort_::rsort(tmp, src, len, 8 * (1 + IS_OVER_THRESHOLD) * (_N / (int)(1 + IS_OVER_THRESHOLD) - 1));
}
};
template struct wrsort_<2, IS_OVER_THRESHOLD> {
template
static void rrsort(_T *src, _T *tmp, int len) {
pre_rsort_::rsort(src, tmp, len, 0); //设定false,不会使用0x10000作为基数
pre_rsort_::rsort(tmp, src, len, 8);
}
};
template struct wrsort_<1, IS_OVER_THRESHOLD> {
template
static void rrsort(_T *src, _T *tmp, int len)
{
pre_rsort_::rsort(src, tmp, len, 0); //设定false,不会使用0x10000作为基数
memcpy(src, tmp, len);
}
};
template struct wrsort_<0, IS_OVER_THRESHOLD> {
template static void rrsort(_T *src, _T *tmp, int len) { };
};
template
void rsort(_T *src, _T *tmp, int len) {
if (len > judgeType::_THRESHOLD)
{
wrsort_< sizeof(_T), true >::rrsort(src, tmp, len);
}
else {
wrsort_< sizeof(_T), false >::rrsort(src, tmp, len);
}
//处理负数部分 --这部分也可是使用模板去实现
if (*(src + len - 1) < 0) {
std::reverse(src, src + len);
std::reverse(std::upper_bound(src, src + len, double(-0.0)), src + len);
}
auto it = std::lower_bound(src, src + len, double(-0.0));
if (*(it - 1) < *(src)) {
std::reverse(src, it);
}
}
上述代码中的 rsort(_T *src, _T *tmp, int len, int bit) 函数中,元素从 源数组 排列到 目数组 时所采用的 映射方法与前面介绍的方法正好相反(前面是从尾部到头部,这里是从尾部到头部),但是排序依然是稳定的(因为每次拷贝一个数 计数数组中的数+1,而前面的方法是-1)。
基数排序的时间复杂度跟基数选取有关,平均复杂度为O(k·n)。基数排序为稳定排序算法。