在所有边角情况下都做到正确有些棘手。如果我必须解决这样的任务,我通常会从一个天真的实现开始,我可以相当确定它是正确的,然后才开始实现一个优化版本。在这样做的同时,我总是可以与天真的方法进行比较,以验证我的结果。
天真的方法是从1开始,并将其乘以/除以2,直到我们将输入的绝对值括起来。然后,我们将输出更接近的边界。实际上,这还有点复杂:如果该值为NaN或无穷大,则需要特殊处理。
以下是代码:
public static double getClosestPowerOf2Loop(final double x) {
final double absx = Math.abs(x);
double prev = 1.0;
double next = 1.0;
if (Double.isInfinite(x) || Double.isNaN(x)) {
return x;
} else if (absx < 1.0) {
do {
prev = next;
next /= 2.0;
} while (next > absx);
} else if (absx > 1.0) {
do {
prev = next;
next *= 2.0;
} while (next < absx);
}
if (x < 0.0) {
prev = -prev;
next = -next;
}
return (Math.abs(next - x) < Math.abs(prev - x)) ? next : prev;
}
我希望代码可以清晰易懂,无需进一步解释。自Java 8以来,您可以使用
!Double.isFinite(x)
替换
Double.isInfinite(x)|| Double.isNaN(x)
。
让我们看一个优化版本。正如其他答案已经建议的那样,我们应该看一下位表示法。Java要求使用IEE 754来表示浮点值。在该格式中,
double
(64位)精度的数字表示为:
我们将再次特殊处理 NaN 和无穷大(它们由特殊的位模式表示)。然而,还有另外一个例外:尾数的最高位是
隐含 的1,并不在位模式中出现 - 除了非常小的数字,这里使用所谓的
亚规范表示,其中最高有效位不是尾数的最高位。因此,对于正常的数字,我们将简单地将尾数的位设置为所有0,但对于亚规范,我们将其转换为一个数字,其中保留了除最高有效的1位之外的其他位。该过程总是向零舍入,因此要得到另一个边界,我们只需乘以2。
让我们看看这些如何协同工作:
public static double getClosestPowerOf2Bits(final double x) {
if (Double.isInfinite(x) || Double.isNaN(x)) {
return x;
} else {
final long bits = Double.doubleToLongBits(x);
final long signexp = bits & 0xfff0000000000000L;
final long mantissa = bits & 0x000fffffffffffffL;
final long mantissaPrev = Math.abs(x) < Double.MIN_NORMAL
? Long.highestOneBit(mantissa)
: 0x0000000000000000L;
final double prev = Double.longBitsToDouble(signexp | mantissaPrev);
final double next = 2.0 * prev;
return (Math.abs(next - x) < Math.abs(prev - x)) ? next : prev;
}
}
我不确定我是否涵盖了所有的角落情况,但以下测试确实可以运行:
public static void main(final String[] args) {
final double[] values = {
5.0, 4.1, 3.9, 1.0, 0.0, -0.1, -8.0, -8.1, -7.9,
0.9 * Double.MIN_NORMAL, -0.9 * Double.MIN_NORMAL,
Double.NaN, Double.MAX_VALUE, Double.MIN_VALUE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
};
for (final double value : values) {
final double powerL = getClosestPowerOf2Loop(value);
final double powerB = getClosestPowerOf2Bits(value);
System.out.printf("%17.10g --> %17.10g %17.10g%n",
value, powerL, powerB);
assert Double.doubleToLongBits(powerL) == Double.doubleToLongBits(powerB);
}
}
输出:
5.000000000 --> 4.000000000 4.000000000
4.100000000 --> 4.000000000 4.000000000
3.900000000 --> 4.000000000 4.000000000
1.000000000 --> 1.000000000 1.000000000
0.000000000 --> 0.000000000 0.000000000
-0.1000000000 --> -0.1250000000 -0.1250000000
-8.000000000 --> -8.000000000 -8.000000000
-8.100000000 --> -8.000000000 -8.000000000
-7.900000000 --> -8.000000000 -8.000000000
2.002566473e-308 --> 2.225073859e-308 2.225073859e-308
-2.002566473e-308 --> -2.225073859e-308 -2.225073859e-308
NaN --> NaN NaN
1.797693135e+308 --> 8.988465674e+307 8.988465674e+307
4.900000000e-324 --> 4.900000000e-324 4.900000000e-324
-Infinity --> -Infinity -Infinity
Infinity --> Infinity Infinity
关于性能如何?
我运行了以下基准测试
public static void main(final String[] args) {
final Random rand = new Random();
for (int i = 0; i < 1000000; ++i) {
final double value = Double.longBitsToDouble(rand.nextLong());
final double power = getClosestPowerOf2(value);
}
}
其中getClosestPowerOf2
将被替换为getClosestPowerOf2Loop
或getClosestPowerOf2Bits
。 在我的笔记本电脑上,我得到了以下结果:
getClosestPowerOf2Loop
:2.35秒
getClosestPowerOf2Bits
:1.80秒
这真的值得努力吗?
12345.4375
。 - durron597