转换浮点数的基数而不会丢失精度

时间:2016-08-01 11:36:55

标签: java algorithm math base decimal-point

术语

在这个问题中,我称之为“浮点数”“十进制数”,以防止使用float / double Java原始数据类型进行ambiguation。术语“十进制”与“基数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,但可以进行协调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]
}

<小时/>

不是作业问题

我不是在找任何图书馆。请不要参考任何图书馆来回答这个问题。(因为我写这个课只是为了好玩,好吗?)

3 个答案:

答案 0 :(得分:5)

对于你的第一个问题:只要旧基数的素因子也是新基数的主要因子,你总是可以转换而不会成为周期性的。例如,每个基数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,......

对于实现,您可以在旧基础(基本上是分区)或新基础(基本上进行乘法)中工作。我会避免在转换期间通过第三个基地。

由于您在问题中添加了定期表示,因此转换的最佳方式似乎是将原始表示转换为分数(这可以始终完成,也适用于周期性表示),然后通过执行将其转换为新表示形式分裂。

答案 1 :(得分:2)

要回答问题的第三部分,一旦你的分数减少了(并且你发现&#34;十进制&#34;扩展将是一个重复的分数),你可以通过简单地做检测重复的部分长期分工并记住你遇到的剩余部分。

例如,要打印基础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

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

所以你的结果将是0. [1031345242 ...]。你可以相当容易地设计一个数据结构来保持这一点,记住在重现开始之前可能有几个数字。您建议的数据结构对此有利。

就我个人而言,我可能只是处理分数,浮点数就是关于紧凑性的一些精度和准确度的交易。如果你不想在精度上妥协,浮点会给你带来很多麻烦。 (虽然经过精心设计,你可以使用它。)

答案 2 :(得分:1)

我在奖励之后等待这一点,因为这不是你问题的直接答案,而是提示如何处理你的任务。

  1. 数字格式

    基本转换期间任意指数形式的数字是一个大问题。相反,我会将您的号码转换/标准化为:

    (sign) mantissa.repetition * base^exp
    

    其中unsigned int expmantissa的最低有效数字的指数。 mantissa,repetition可以是字符串,便于操作和打印。但这会限制你粗略的最大基数。例如,如果您为exponent保留e,则可以使用{ 0,1,2,..9, A,B,C,...,Z }作为数字,因此最大基数将仅为36(如果不计算特殊字符)。如果这还不够,请使用int数字表示。

  2. 基本转化(尾数)

    我现在将尾数作为整数处理。因此,只需在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;
        }
    //---------------------------------------------------------------------------
    

    C ++ VCL 编写。 AnsiString VCL 字符串类型,具有自我分配属性,其成员来自1

  3. 基本转化(重复)

    我知道有两种方法。更简单但可能有圆错误的是将重复设置为足够长的字符串序列并将其作为小数处理。例如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形式。您只需将ab转换为整数(与尾数相同),然后进行除法以获得小数部分+重复部分。

    所以现在你应该在表格中转换了数字:

    mantissa.fractional [new_base] * old_base^exp
    

    或:

    mantissa.fractional+a/b [new_base] * old_base^exp
    
  4. 基本转化(指数)

    您需要将old_base^old_exp更改为new_base^new_exp。最简单的方法是将数字乘以新的基础算术中的old_base^old_exp值。所以对于初学者来说,乘以整个

    mantissa.fractional+(a/b) [new_base]
    

    在新算法中old_base old_exp次(之后您可以通过平方或更好的方式将其更改为幂)。然后将你的号码标准化。因此,找到重复字符串开始的位置,其相对于.的数字位置是new_exp值。

  5. <强> [注释]

    为此你需要例程来相互转换old_basenew_base但是因为基础不是bignum而是简单的小unsigned int而不是它对你来说不应该是任何问题(我希望)。