在不损失精度的情况下转换浮点数的基数

2022-09-03 15:28:50

术语

在这个问题中,我将“浮点数”称为“十进制数”,以防止与/ Java基元数据类型混淆。术语“十进制”与“以 10 为基数”没有关系。floatdouble

背景

我以这种方式表示任何基数的十进制数:

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;
}

我知道有些转换是不可能的。例如,无法转换为以 10 为基数,因为它是重复出现的小数。同样,转换为基数 3 是不可能的,但 covnerting 是可能的。可能还有其他我没有考虑过的情况。0.1 (base 3)0.1 (base 9)0.3 (base 3)

传统方式

对于整数,从以10为基数到以2为基数的整数,传统的(手工)变化方法是将数字除以2的指数,从基数2到基数10是将数字乘以2的相应指数。从基数 x 更改为以 y 为基数通常涉及转换为以 10 为基数作为中间值。

第一个问题:参数验证

因此,我的第一个问题是,如果我要实现该方法,我如何验证是否可以在不导致重复小数的情况下进行(这与字段的设计不兼容,因为我不打算为此制作字段。public Decimal Decimal.changeBase(int newBase)newBaseint[] digitsint recurringOffset

第二个问题:执行

那么,如何实现这一点呢?我本能地觉得,如果第一个问题解决了,这个问题就更容易解决了。

第三个问题:重复出现的数字输出呢:

我不打算仅仅为此做一个领域。int recurringOffset

为了未来的读者,这个问题也应该问。

例如,根据Wolfram的说法|阿尔法

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

为了回答问题的第三部分,一旦你减少了分数(并且你发现“十进制”扩展将是一个重复的分数),你可以通过简单地做长手除法并记住你遇到的余数来检测重复的部分。

例如,要以 6 为基数打印输出,请执行以下操作:2/11

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

(如果可以转换为有限长度的以 6 为底数的数,我们将达到 0 余数。2/11

因此,您的结果将为 0。[1031345242...]您可以相当容易地设计一个数据结构来保存它,请记住,在重复开始之前可能有几个数字。您建议的数据结构对此很有帮助。

就个人而言,我可能只使用分数,浮点数就是为了换取一些精度和准确性以获得紧凑性。如果您不想在精度上妥协,浮点数会给您带来很多麻烦。(虽然通过精心的设计,你可以走得很远。