基数排序——浮点数结构体进阶
基数排序——浮点数结构体进阶
前置
这个东西曾经在我的Luogu Blog写过,所以尚未学过基数排序请先使用这篇文章入门。
上面讲到了一种对于整数划分成二进制进行基数排序的方法,并且用结构体指针转换为整型指针来进行强制类型转换把这个方法扩展到了结构体上。
但是当时这个方法存在很大的局限性,就是它必须依赖于整数,这也就意味着结构体大小最大为64bit,但是我们考虑即使使用pimpl手法(也就是一个指针指向另外一个结构体)的结构体,在64bit机子下的指针大小64bit,那么这样就没什么用了。
然后最近重新思考了一下指针在这种基数排序内的作用,然后稍微了解了一下浮点数的原理,于是就想到了一种对于浮点数进行基数排序的方法。
结构体
我们注意到,我们之前使用结构体指针转整数指针对结构体进行了排序,但是如果待排序位数大于64bit的话那么long long无法完全表示这个结构体,例如两个long long需要进行双关键字排序那么就会咕咕。
于是我们考虑上面的指针。我们发现指针是可以移动的,也就是可以通过++ --来移动到想要的内存位置。那么我们是不是可以通过用一个char指针先指向一位,然后逐位扫描呢?是可以的。因为每次基数排序的时候如果桶的大小是一个字节的话这样就可以了。
于是我们考虑如下代码
1 template <typename T> 2 inline void Radix_Sort_Bit(register const int n, T *a, T *b){ 3 size_t size_of_type = sizeof(a); 4 size_t num_of_buc = ((size_of_type >> 1) + 1) >> 1; 5 unsigned r[num_of_buc][0x10000]; 6 memset(r, 0, sizeof(r)); 7 register int i, k; 8 register unsigned short tmp_us; 9 register T * j, *tar; 10 for(k = 0; k < num_of_buc; ++k){ 11 for(j = a + 1, tar = a + 1 + n; j != tar; ++ j){ 12 tmp_us = * (((unsigned short *)j) + k); 13 ++ r[k][tmp_us]; 14 } 15 } 16 for(k = 0; k < num_of_buc; k++) 17 for(i = 1; i <= 0xffff; i++) 18 r[k][i] += r[k][i - 1]; 19 for(k = 0; k < num_of_buc; k += 0x2){ 20 i = k; 21 for(j = a + n; j != a; --j){ 22 tmp_us = * (((unsigned short *)j) + i); 23 b[r[i][tmp_us]--] = *j; 24 } 25 i |= 1; 26 for(j = b + n; j != b; --j){ 27 tmp_us = * (((unsigned short *)j) + i); 28 a[r[i][tmp_us]--] = *j; 29 } 30 } 31 }
然后上面代码适用于结构体大小不确定的情况下,如果结构体大小确定那么就可以手动进行下列优化
1. 数组开外面或者栈内
2. 适当循环展开
3. 更改二进制拆分
同时这份代码适用于小端法机器(Link),如果是大端法机器的话注意k的循环顺序
浮点数
下面说的“实数”指的是数学意义上的,“浮点数”表示计算机储存的“实数”
好像基数排序在一开始就不方便做实数,一个指定的整数范围内的实数就是无限多的,然后浮点数后面又会有高精度位什么的会降低基数排序效率,而且十进制对浮点数的提取也比较困难,因为实际上0.1在计算机中无法精确表示。
但是IEEE 754给二进制浮点数一些非常优美的性质
二进制浮点数被设计成用$s * 2 ^ E * M$表示,其中s是符号位,即采用整数中原码的表示正负的方式,E是阶码,M是尾码
一个C++ 中的double被表示成二进制的方法如下:
By Codekaizen - Own work, CC BY-SA 4.0, Link
然后具体的编码方式不在此解释,可以查看上面IEEE 754链接
然后我们要用的性质
如果有两个浮点数a, b,且他们不是NAN或者INFINITY,浮点数大小f(a), f(b),如果忽略符号位,那么剩下的位在解释为无符号整数编码后得到无符号数w(a), w(b),满足若w(a) <= w(b),则f(a) <= f(b)
于是我们就可以得到一种排序思路:先带符号位排序,然后对负数部分特殊处理
同时我们注意到float 32bit, double 64bit, long double 80bit(stored as 96bit(x86) or 128bit(x64)), __float128 128bit,这样的话如果用超过64位的浮点数好像会不便于排序
但是我们可以像上面一样像结构体一样排序,然后最后处理一下符号位即可(直接使用stl的reverse)
1 inline void Radix_Sort_Double(register const int n, double *a, double *b){ 2 Radix_Sort_64Bit(n, a, b); 3 reverse(a + 1, a + 1 + n); 4 reverse(upper_bound(a + 1, a + 1 + n, double(-0.0)), a + 1 + n); 5 }
最后一个有意思的事情是通过这种方法可以得到如下序列
-inf -inf -234.000000 -234.000000 -123.000000 -123.000000 -0.100000 -0.100000 -0.000000 -0.000000 nan nan 0.000000 0.000000 0.100000 0.100000 123.000000 123.000000 234.000000 234.000000 inf inf
-0.0 < nan < 0.0
非常优美,如果使用stl的sort的话对inf和nan的处理都不够好