基本排序算法 之十 ——基数排序

 


基本排序算法总结与对比 之十  ——基数排序 


首先 基数排序分为 两类:

       第一类:最低位优先法,简称LSD法:先从最低位开始排序,再对次低位排序,直到对最高位排序后得到一个有序序列。本文将介绍这种方法。

       第二类:最高位优先法,简称MSD法:先从最高位开始排序,再逐个对各分组内部按次高位进行子排序,循环直到最低位。 这种方法的优势是可以处理长度不一致的数据,也就是适合排序字符串;缺点就是不好实现。这种方法本文不做介绍。     

1、基数排序  以10为基数 版本

      下图是 最容易理解的 以 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、基数排序  以2的倍数 为基数 版本

       直接跳到核心部分。以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)。基数排序为稳定排序算法。

你可能感兴趣的:(数据结构与算法基础,排序算法,基数排序)