Сделать это правильно во всех крайних случаях немного сложно. Если мне нужно решить такую задачу, я обычно начинаю с наивной реализации, в которой могу быть уверен, что она будет правильной, и только потом приступаю к реализации оптимизированной версии. При этом я всегда могу сравнить с наивным подходом, чтобы подтвердить свои результаты.
Наивный подход состоит в том, чтобы начать с 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 бита) представляются как
- 1 битный знак,
- 11-битная экспонента и
- 52-битная мантисса.
Мы снова будем использовать специальные регистры 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 с
Стоило ли это усилий?
person
5gon12eder
schedule
30.01.2015
12345.4375
- person durron597   schedule 30.01.2015