排序算法之:利用基数排序处理多精度浮点数与科学计数法表示的数字排序
题目描述
我们通常使用基数排序处理整数,但面对科学计算或工程中的数据,常常需要排序多精度浮点数(如双精度浮点数)或科学计数法表示的数字(如 "3.14e+10", "-1.6e-19")。本题要求设计一个基于基数排序的算法,能够稳定、高效地对这类数字进行排序。
输入:一个数组,元素可以是多精度浮点数(例如 64 位 double 类型)或科学计数法表示的字符串。
输出:按数值大小非递减排序的数组。
约束:
- 算法必须是稳定的(即数值相等的元素保持原有相对顺序)。
- 不能直接使用语言内置的浮点数比较排序(如
std::sort),但可利用其位运算或字节表示。 - 需处理正数、负数、零以及特殊值(如 Inf, NaN,但为简化,本题可先假设输入为有限值)。
- 时间复杂度和空间复杂度尽可能低,例如 O(n * k),其中 k 是与数字表示相关的“位数”(如字节数)。
解题过程
第一步:理解浮点数的内存表示与排序难点
浮点数(以 IEEE 754 双精度为例)在内存中占 64 位,包括:
- 1 位符号位 S(0 正,1 负)
- 11 位指数位 E(偏移 1023)
- 52 位尾数位 M
关键难点:
- 负数的表示不是简单的补码,因此其二进制位与数值大小顺序不直接对应。
- 科学计数法字符串如 "2.5e3" 需要解析为数值,再转换为内存表示以便排序。
- 排序必须是数值顺序,而非字典序(例如 "-1.2" 应小于 "0.5")。
第二步:核心思路——将浮点数映射为可排序的整数
观察 IEEE 754 表示,如果将整个 64 位二进制视为有符号整数,会发现一个重要性质:
- 对于正浮点数(S=0),其二进制对应的整数是单调递增的(因为指数 E 和尾数 M 越大,数值越大)。
- 对于负浮点数(S=1),其二进制对应的整数也是单调递增的(但负数在内存中是以“符号-幅度”加偏移存储的,其二进制对应整数与数值大小方向一致)。
更精确地说,如果定义:
uint64_t bits = * (uint64_t*) &double_value;
则对于任意两个浮点数 a, b:
- 若 a, b 均为正数:a < b ⇔ bits_of(a) < bits_of(b)
- 若 a, b 均为负数:a < b ⇔ bits_of(a) < bits_of(b)
- 若 a 负, b 正:必然 a < b,但 bits_of(a) 的最高位为 1,bits_of(b) 最高位为 0,所以 bits_of(a) 在无符号比较时大于 bits_of(b)。
为了使得整数顺序与数值顺序完全一致,我们需要对符号位进行反转:
- 如果符号位 S=1(负数),将其余 63 位取反(或更简单的:将整个 64 位与 0x8000... 异或,仅翻转符号位?)。
- 如果符号位 S=0(正数),将其余 63 位保持不变,但将符号位设为 1?让我们仔细推导。
更标准的映射方法(用于基数排序):
定义转换函数 float_to_sortable_int(x):
- 取 x 的 64 位表示
bits。 - 如果
bits的最高位(符号位)为 0(正数),则将其最高位设为 1(即加上 0x8000000000000000)。 - 如果
bits的最高位为 1(负数),则将其所有位取反(即与 0xFFFFFFFFFFFFFFFF 异或)。
这样得到的整数key,满足:
对于任意两个浮点数 a, b:
a < b ⇔ key(a) < key(b)
证明:
- 正数范围:原表示中,正数的二进制整数形式是递增的,但最高位为 0。加上 0x8000... 后,它们映射到 [0x8000..., 0xFFFF...],仍保持递增。
- 负数范围:原表示中,负数的二进制整数形式是递增的(因为 S=1 固定,E 和 M 越大,数值越大),但此时数值是负的。对整个 bits 取反后,原来小的负数(如 -100)会变成大的整数吗?实际上,取反相当于对负数做了“绝对值从小到大”的映射,并且使得所有负数映射后的整数小于正数映射后的整数。
更简单的等价做法(实际常用):
uint64_t key = bits ^ (1 - (bits >> 63)) * 0xFFFFFFFFFFFFFFFF;
但更直接的方法是:
if (bits & 0x8000000000000000) // 负数
key = ~bits;
else // 正数(包括0)
key = bits ^ 0x8000000000000000;
这样,key 是一个 64 位无符号整数,其顺序与原始浮点数数值顺序相同。
第三步:算法框架
- 将每个浮点数通过上述映射转换为 64 位无符号整数 key。
- 对 key 数组使用基数排序(按字节或按 16 位块等)。
- 排序完成后,将 key 通过逆映射转换回浮点数。
逆映射:
if (key & 0x8000000000000000) // 原来为正数
bits = key ^ 0x8000000000000000;
else // 原来为负数
bits = ~key;
第四步:处理科学计数法字符串
如果输入是科学计数法字符串,例如 "3.14e-5":
- 使用标准库(如
stod)将其解析为双精度浮点数。 - 然后按上述浮点数方法处理。
注意:如果要求不解析为浮点数(避免精度损失),可以直接处理字符串,但那样会变为字符串排序而非数值排序,不符合本题要求。因此我们假设解析是允许的。
第五步:基数排序的实现细节
我们采用 LSD(Least Significant Digit)基数排序,因为它是稳定的,且适合整数排序。
- 基数(桶数)选择 256(每次处理 1 字节)或 65536(每次处理 2 字节)。
- 步骤:
a. 创建计数数组count[256]和临时数组temp[n]。
b. 对 64 位 key 的每个字节(从低字节到高字节)进行计数排序。
c. 每轮统计频次、计算前缀和、将元素按当前字节值放入临时数组正确位置。
d. 重复 8 轮(因为 64 位 = 8 字节)。
代码框架(伪代码):
function radix_sort_float(arr):
n = length(arr)
keys = array of uint64[n]
for i = 0 to n-1:
bits = reinterpret arr[i] as uint64
keys[i] = convert_to_sortable_int(bits)
// LSD 基数排序(基数 256)
for shift = 0, 8, 16, ..., 56:
count[256] = {0}
for i = 0 to n-1:
byte = (keys[i] >> shift) & 0xFF
count[byte]++
// 前缀和
total = 0
for b = 0 to 255:
old = count[b]
count[b] = total
total += old
// 放置元素
for i = 0 to n-1:
byte = (keys[i] >> shift) & 0xFF
temp[count[byte]] = keys[i]
count[byte]++
keys = temp
// 转换回原数组
for i = 0 to n-1:
bits = convert_from_sortable_int(keys[i])
arr[i] = reinterpret bits as double
return arr
第六步:稳定性与特殊值处理
- 稳定性:LSD 基数排序是稳定的,只要每轮使用稳定的计数排序即可。
- 特殊值:
- 零:+0 和 -0 的表示不同,但数值相等。上述映射能保持它们相等吗?+0 的二进制为全 0,-0 的符号位为 1 其余为 0。映射后 key 可能不同,但排序时它们可能被分开。如果要求 +0 和 -0 视为相等且保持原序,则需要特殊处理(例如在映射前统一为 +0)。
- Inf 和 NaN:本题可先假设输入为有限值,但若要支持,需定义顺序(如 -Inf < 有限数 < +Inf,NaN 可视为最大并放在末尾)。
第七步:复杂度分析
- 时间:O(n * 8) = O(n)(因为 8 轮,每轮 O(n+256))。
- 空间:O(n) 用于临时数组,以及 O(256) 计数数组。
第八步:扩展思考
- 如果输入是任意精度的十进制字符串(如 "123.4567890123456789"),无法精确转为双精度,则需直接对字符串做基数排序,但必须按数值比较。此时可以将字符串转为固定点数(例如放大为整数)或使用多精度库。
- 如果要求支持 NaN 和 Inf,可以在映射时将它们映射到整数范围的最高端,并确保 NaN 不参与比较(但排序时需特殊处理)。
总结
本题通过位运算将浮点数映射为顺序保持的整数,再对整数进行稳定的 LSD 基数排序,最后逆映射恢复浮点数。该算法保证了 O(n) 的时间复杂度(常数较大)和稳定性,适用于多精度浮点数及科学计数法表示的数字排序。