#c
#c
Вопрос:
Мне нужно представлять числа, используя следующую структуру. Цель этой структуры — не потерять точность.
struct PreciseNumber
{
long significand;
int exponent;
}
Используя эту структуру, фактическое двойное значение может быть представлено как value = significand * 10e^exponent
.
Теперь мне нужно написать служебную функцию, которая может скрывать double в PreciseNumber.
Не могли бы вы, пожалуйста, сообщить мне, как извлечь показатель степени и значение из double?
Комментарии:
1. У вас фундаментальное недоразумение. Представление с плавающей запятой по основанию 10 (что и есть это) не более и не менее точно, чем представление с плавающей запятой по основанию 2. Однако преобразование из одного в другое — верный способ потерять точность.
2. Не изобретайте велосипед. gmplib.org
3. Помните: компьютеры отстой в математике! techradar.com/news/computing/why-computers-suck-at-maths-644771
4. @Oli: используя long long, вы можете получить 18 точных цифр, что немного больше, чем может предложить double (я думаю, что это 16)
5. @Andrey: Это правда. Но я думаю, что у OP создается впечатление, что base-10 каким-то образом по своей сути более точен.
Ответ №1:
Прелюдия несколько испорчена.
Во-первых, за исключением каких-либо ограничений на пространство для хранения, преобразование из формы double в форму 10 по основанию со значением и показателем степени не изменит точность ни в какой форме. Чтобы понять это, рассмотрите следующее: любая двоичная конечная дробь (например, та, которая образует мантиссу в типичном IEEE-754 с плавающей точкой) может быть записана как сумма отрицательных степеней двойки. Каждая отрицательная степень двойки сама по себе является завершающей дробью, и, следовательно, из этого следует, что их сумма также должна быть завершающей.
Однако обратное не обязательно верно. Например, 0.3
основание 10 эквивалентно не завершающемуся 0.01 0011 0011 0011 ...
в базе 2. Установка этого в мантиссу фиксированного размера снизила бы ее точность (вот почему 0.3
фактически сохраняется как нечто, что преобразуется обратно в 0.29999999999999999
.)
Таким образом, мы можем предположить, что любая точность, которая предназначена для хранения чисел в десятичной форме значение-показатель степени, либо теряется, либо просто не достигается вообще.
Конечно, вы могли бы подумать о кажущейся потере точности, вызванной сохранением десятичного числа в виде числа с плавающей запятой, как о потере точности, и в этом случае форматы Decimal32 и Decimal64 с плавающей запятой могут представлять некоторый интерес — ознакомьтесьhttp://en.wikipedia.org/wiki/Decimal64_floating-point_format .
Ответ №2:
Это очень сложная проблема. Возможно, вы захотите посмотреть, сколько кода требуется для реализации преобразования double в string (например, для printf). Вы можете украсть код из реализации gcc gnu.
Ответ №3:
Вы не можете преобразовать «неточный» double в «точное» десятичное число, потому что требуемой «точности» просто нет для начала (иначе зачем бы вам вообще понадобилось конвертировать?).
Вот что произойдет, если вы попробуете что-то подобное на Java:
BigDecimal x = new BigDecimal(0.1);
System.out.println(x);
Результатом программы является:
0.1000000000000000055511151231257827021181583404541015625
Комментарии:
1. Не совсем верно. Вы можете округлить «неточное» число до ближайшего точного представления, если вы знаете, сколько цифр гарантировано.
2. @Fred: ты не используешь неправильный вид буквы «С»? на плакате было написано C
3. @Andrey: Это фундаментальная численная проблема, которая не имеет ничего общего с языками программирования.
4. @Mark Но откуда ты знаешь, что округленное число лучше того, с которого ты начинал?
5. Тогда зачем публиковать код? (Java, а не C #, извините за это). Я думаю, дело в том, что некоторые числа имеют конечное представление в базе 10, но не одно в базе 2 (и наоборот).
Ответ №4:
Ну, у вас меньшая точность, чем у обычного double. Ваше значение является длинным, что дает вам диапазон от -2 миллиардов до 2 миллиардов, что составляет более 9, но менее 10 цифр точности.
Вот непроверенная отправная точка для того, что вы хотели бы сделать для некоторой простой математики с точными числами
PreciseNumber Multiply(PreciseNumber lhs, PreciseNumber rhs)
{
PreciseNumber ret;
ret.s=lhs.s;
ret.e=lhs.e;
ret.s*=rhs.s;
ret.e =lhs.e;
return ret;
}
PreciseNumber Add(PreciseNumber lhs, PreciseNumber rhs)
{
PreciseNumber ret;
ret.s=lhs.s;
ret.e=lhs.e;
ret.s =(rhs.s*pow(10,rhs.e-lhs.e));
}
Я не позаботился о какой-либо перенормировке, но в обоих случаях есть места, где вам приходится беспокоиться о чрезмерных / недостаточных потоках и потере точности. Только потому, что вы делаете это сами, а не позволяете компьютеру позаботиться об этом в двойном режиме, не означает, что там нет тех же самых подводных камней. Единственный способ не потерять точность — отслеживать все цифры.
Ответ №5:
Вот очень грубый алгоритм. Я попытаюсь заполнить некоторые детали позже.
Возьмите log10 числа, чтобы получить показатель степени. Умножьте double на 10 ^ x, если он положительный, или разделите на 10 ^-x, если отрицательный.
Начните со значения, равного нулю. Повторите следующие 15 раз, поскольку double содержит 15 цифр значения:
- Умножьте предыдущее значение на 10.
- Возьмите целочисленную часть double, добавьте ее к значимому и вычтите из double.
- Вычтите 1 из показателя степени.
- Умножьте двойное значение на 10.
Когда закончите, возьмите оставшееся двойное значение и используйте его для округления: если это >= 5
, добавьте единицу к значимому.