Преобразование базы чисел с плавающей запятой без потери точности
Терминология
В этом вопросе я вызываю "число с плавающей запятой" "десятичное число", чтобы предотвратить неоднозначность с примитивными типами данных float
/double
Java. Термин "десятичный" не имеет отношения к "основанию 10".
Фон
Я выражаю десятичное число любой базы таким образом:
class Decimal{
int[] digits;
int exponent;
int base;
int signum;
}
который приблизительно выражает это значение double
:
public double toDouble(){
if(signum == 0) return 0d;
double out = 0d;
for(int i = digits.length - 1, j = 0; i >= 0; i--, j++){
out += digits[i] * Math.pow(base, j + exponent);
}
return out * signum;
}
Я знаю, что некоторые преобразования невозможны. Например, преобразование 0.1 (base 3)
в базу 10 невозможно, поскольку оно является повторяющимся десятичным. Точно так же преобразование 0.1 (base 9)
в базу 3 невозможно, но возможно covnerting 0.3 (base 3)
. Вероятно, есть другие случаи, которые я не рассматривал.
Традиционный способ
Традиционный способ (вручную) изменения базы для целых чисел от основания 10 до основания 2 состоит в том, чтобы разделить число на показатели 2, а от основания 2 до основания 10 - умножить цифры на соответствующие показателями 2. Переход от основания x к основанию y обычно включает преобразование в основание 10 в качестве промежуточного элемента.
Первый вопрос: проверка аргумента
Поэтому мой первый вопрос заключается в том, что если я должен реализовать метод public Decimal Decimal.changeBase(int newBase)
, как я могу проверить, можно ли сделать newBase
без повторения десятичных знаков (что несовместимо с конструкцией поля int[] digits
, так как я не планирую создать поле int recurringOffset
только для этого.
Второй вопрос: реализация
Следовательно, как это реализовать? Я инстинктивно чувствую, что этот вопрос намного легче решить, если первый вопрос будет решен.
Третий вопрос: как насчет вывода повторяющихся номеров:
Я не планирую создавать поле int recurringOffset
только для этого.
Для будущих читателей этот вопрос также следует задать.
Например, согласно Wolfram | Alpha:
0.1 (base 4) = 0.[2...] (base 9)
Как это можно вычислить (вручную, если по программированию звучит слишком сложно)?
Я думаю, что такая структура данных, как это, может представлять это десятичное число:
class Decimal{
int[] constDigits;
int exponent;
int base;
int signum;
@Nullable @NonEmpty int[] appendRecurring;
}
Например, 61/55
может быть выражен следующим образом:
{
constDigits: [1, 1], // 11
exponent: -1, // 11e-1
base: 10,
signum: 1, // positive
appendRecurring: [0, 9]
}
Не вопрос о домашнем задании
Я не ищу никаких библиотек. Пожалуйста, не отвечайте на этот вопрос со ссылкой на любые библиотеки. (потому что я пишу этот класс просто для удовольствия, ОК?)
Ответы
Ответ 1
К вашему первому вопросу: всякий раз, когда основные факторы старой базы также входят в число основных факторов новой базы, вы всегда можете конвертировать, не становясь периодическими. Например, каждое базовое число 2 может быть представлено точно как основание 10. Это условие, к сожалению, является достаточным, но не необходимым, например, есть некоторые базовые 10 чисел, такие как 0,5, которые могут быть представлены точно как основание 2, хотя 2 не имеет простого коэффициента 5.
Когда вы записываете число как дробь и сводите его к младшим, он может быть представлен точно без периодической части в базе x тогда и только тогда, когда знаменатель имеет только простые множители, которые также появляются в x (игнорируя показатели простых чисел).
Например, если ваш номер равен 3/25, вы можете представить это точно в каждой базе, которая имеет основной коэффициент 5. Это 5, 10, 15, 20, 25,...
Если число равно 4/175, знаменатель имеет простые коэффициенты 5 и 7 и поэтому может быть представлен точно в основании 35, 70, 105, 140, 175,...
Для реализации вы можете либо работать на старой базе (в основном, делать дивизии), либо на новой базе (в основном делать умножения). Я бы избежал прохождения третьей базы во время преобразования.
Поскольку вы добавили периодические представления к своему вопросу, лучшим способом преобразования, по-видимому, является преобразование исходного представления в дробь (это всегда можно сделать и для периодических представлений), а затем преобразовать его в новое представление, выполнив разделение.
Ответ 2
Чтобы ответить на третью часть вопроса, как только вы уменьшите свою долю (и вы обнаружили, что "десятичное" расширение будет повторяющейся долей), вы можете обнаружить повторяющуюся часть, просто сделав длинное разделение и запоминание остатков, с которыми вы столкнулись.
Например, чтобы напечатать 2/11
в базе 6, вы выполните следующее:
2/11 = 0 (rem 2/11)
2*6/11 = 1 (rem 1/11)
1*6/11 = 0 (rem 6/11)
6*6/11 = 3 (rem 3/11)
3*6/11 = 1 (rem 7/11)
7*6/11 = 3 (rem 9/11)
9*6/11 = 4 (rem 10/11)
10*6/11 = 5 (rem 5/11)
5*6/11 = 2 (rem 8/11)
8*6/11 = 4 (rem 4/11)
4*6/11 = 2 (rem 2/11) <-- We've found a duplicate remainder
(Если бы 2/11
было конвертировано в базовое число 6 конечной длины, мы бы достигли 0 остатков вместо.)
Итак, ваш результат будет 0. [1031345242...]. Вы можете довольно легко создать структуру данных для ее хранения, учитывая, что до начала повторения может быть несколько цифр. Ваша предлагаемая структура данных хороша для этого.
Лично я, вероятно, просто работаю с фракциями, с плавающей точкой - это торговля с определенной точностью и точностью для компактности. Если вы не хотите идти на компромисс с точностью, с плавающей точкой будет много проблем. (Хотя с осторожным дизайном вы можете получить довольно далеко от него.)
Ответ 3
Я ждал этого после награды, потому что это не прямой ответ на ваши вопросы, а несколько советов, как подойти к вашей задаче.
-
Формат номера
Произвольная экспоненциальная форма числа при базовом преобразовании является большой проблемой. Вместо этого я бы преобразовал/нормализовал ваш номер в форму:
(sign) mantissa.repetition * base^exp
Где unsigned int exp
- показатель наименьшей значащей цифры mantissa
. mantissa,repetition
может быть строкой для простой манипуляции и печати. Но это ограничило бы вашу максимальную базу грубого. Например, если вы зарезервируете e
для экспоненты, вы можете использовать { 0,1,2,..9, A,B,C,...,Z }
для цифр, поэтому максимальная база будет тогда только 36
(если не считать специальные символы). Если этого недостаточно, оставайтесь с цифровым представлением int
.
-
Базовое преобразование (мантисса)
Я бы обработал мантисса как целое число. Таким образом, преобразование выполняется просто путем деления mantissa / new_base
на арифметику old_base
. Это можно сделать непосредственно на строках. С этим нет никаких проблем, так как мы всегда можем преобразовать любое целое число из любой базы в любую другую базу без каких-либо несоответствий, округления или остатков. Преобразование может выглядеть так:
// convert a=1024 [dec] -> c [bin]
AnsiString a="1024",b="2",c="",r="";
while (a!="0") { a=divide(r,a,b,10); c=r+c; }
// output c = "10000000000"
Где:
-
a
- это номер в старой базе, который вы хотите преобразовать
-
b
- новая база в старом базовом представлении
-
c
- номер в новой базе
Используемая функция деления выглядит следующим образом:
//---------------------------------------------------------------------------
#define dig2chr(x) ((x<10)?char(x+'0'):char(x+'A'-10))
#define chr2dig(x) ((x>'9')?BYTE(x-'A'+10):BYTE(x-'0'))
//---------------------------------------------------------------------------
int compare( const AnsiString &a,const AnsiString &b); // compare a,b return { -1,0,+1 } -> { < , == , > }
AnsiString divide(AnsiString &r,const AnsiString &a, AnsiString &b,int base); // return a/b computed in base and r = a%b
//---------------------------------------------------------------------------
int compare(const AnsiString &a,const AnsiString &b)
{
if (a.Length()>b.Length()) return +1;
if (a.Length()<b.Length()) return -1;
for (int i=1;i<=a.Length();i++)
{
if (a[i]>b[i]) return +1;
if (a[i]<b[i]) return -1;
}
return 0;
}
//---------------------------------------------------------------------------
AnsiString divide(AnsiString &r,const AnsiString &a,AnsiString &b,int base)
{
int i,j,na,nb,e,sh,aa,bb,cy;
AnsiString d=""; r="";
// trivial cases
e=compare(a,b);
if (e< 0) { r=a; return "0"; }
if (e==0) { r="0"; return "1"; }
// shift b
for (sh=0;compare(a,b)>=0;sh++) b=b+"0";
if (compare(a,b)<0) { sh--; b=b.SetLength(b.Length()-1); }
// divide
for (r=a;sh>=0;sh--)
{
for (j=0;compare(r,b)>=0;j++)
{
// r-=b
na=r.Length();
nb=b.Length();
for (i=0,cy=0;i<nb;i++)
{
aa=chr2dig(r[na-i]);
bb=chr2dig(b[nb-i]);
aa-=bb+cy; cy=0;
while (aa<0) { aa+=base; cy++; }
r[na-i]=dig2chr(aa);
}
if (cy)
{
aa=chr2dig(r[na-i]);
aa-=cy;
r[na-i]=dig2chr(aa);
}
// leading zeros removal
while ((r.Length()>b.Length())&&(r[1]=='0')) r=r.SubString(2,r.Length()-1);
}
d+=dig2chr(j);
if (sh) b=b.SubString(1,b.Length()-1);
while ((r.Length()>b.Length())&&(r[1]=='0')) r=r.SubString(2,r.Length()-1);
}
return d;
}
//---------------------------------------------------------------------------
Он написан в С++ и VCL. AnsiString
- это строковый тип VCL с самораспределяющими свойствами, а его члены индексируются из 1
.
-
Базовое преобразование (повторение)
Есть два подхода к этому, о которых я знаю. Более простые, но с возможными круглыми ошибками устанавливают повторение на достаточно длинную последовательность строк и обрабатывают как дробное число. Например, rep="123" [dec]
, тогда преобразование в другую базу будет выполнено путем умножения на новую базу в старой базовой арифметике. Поэтому создадим достаточно длинную последовательность:
0 + 0.123123123123123 * 2
0 + 0.246246246246246 * 2
0 + 0.492492492492492 * 2
0 + 0.984984984984984 * 2
1 + 0.969969969969968 * 2
1 + 0.939939939939936 * 2
1 + 0.879879879879872 * 2 ...
------------------------------
= "0.0000111..." [bin]
С помощью этого шага вам необходимо выполнить повторный анализ и нормализовать число снова после шага коррекции экспоненты (в следующей броне).
Второй подход должен иметь повторения, хранящиеся как деление, поэтому вам нужно его в форме a/b
в old_base
. Вы просто конвертируете a
, b
в виде целых чисел (так же, как и мантисса), а затем выполняете деление для получения частичной части + части повторения.
Итак, теперь вам нужно преобразовать число в форму:
mantissa.fractional [new_base] * old_base^exp
или
mantissa.fractional+a/b [new_base] * old_base^exp
-
Базовое преобразование (экспонента)
Вам нужно изменить old_base^old_exp
на new_base^new_exp
. Самый простой способ - умножить число на значение old_base^old_exp
в новой базовой арифметике. Итак, для начинающих умножьте все
mantissa.fractional+(a/b) [new_base]
на old_base
old_exp
раз в новой арифметике (позже вы можете изменить ее на мощность, возведя квадрат или лучше). И после этого нормализуйте свой номер. Итак, найдите, где начинается строка повторения, и ее позиция по отношению к .
- это значение new_exp
.
[Примечание]
Для этого вам понадобятся подпрограммы для преобразования old_base
и new_base
между собой, но поскольку база не является bignum, а просто простой небольшой unsigned int, вместо этого это не должно быть проблемой для вас (я надеюсь).