【问题标题】:Arbitrary-precision arithmetic Explanation任意精度算术说明
【发布时间】:2010-11-16 03:09:28
【问题描述】:

我正在尝试学习 C,但遇到了无法处理非常大的数字(即 100 位、1000 位等)的问题。我知道存在执行此操作的库,但我想尝试自己实现它。

我只是想知道是否有人已经或可以提供对任意精度算术的非常详细、简单的解释。

【问题讨论】:

    标签: math biginteger bignum


    【解决方案1】:

    这完全取决于足够的存储空间和算法来将数字视为较小的部分。假设您有一个编译器,其中 int 只能是 0 到 99,并且您希望处理最大为 999999 的数字(为了简单起见,我们只担心正数)。

    您可以通过给每个数字三个ints 来做到这一点,并使用您(应该)在小学学习的相同规则进行加法、减法和其他基本运算。

    在任意精度库中,用于表示我们的数字的基本类型的数量没有固定限制,只要内存可以容纳。

    加法举例:123456 + 78:

    12 34 56
          78
    -- -- --
    12 35 34
    

    从最不重要的一端开始工作:

    • 初始进位 = 0。
    • 56 + 78 + 0 进位 = 134 = 34 有 1 个进位
    • 34 + 00 + 1 进位 = 35 = 35 与 0 进位
    • 12 + 00 + 0 进位 = 12 = 12 与 0 进位

    事实上,这就是加法通常在 CPU 中的位级别上的工作方式。

    减法类似(使用基本类型的减法和借位而不是进位),乘法可以通过重复加法(非常慢)或叉积(更快)来完成,除法更棘手,但可以通过移位和减法来完成所涉及的数字(您小时候会学习的长除法)。

    我实际上已经编写了库来使用最大的 10 次方来做这类事情,当平方时可以适合整数(以防止在将两个 ints 相乘时溢出,例如 16 位 @ 987654328@ 被限制为 0 到 99 以在平方时生成 9,801 (int 使用 0 到 9,999 生成 99,980,001 (

    需要注意的一些技巧。

    1/ 加法或乘法时,预先分配所需的最大空间,如果发现太多,以后再减少。例如,添加两个 100-"digit"(其中 digit 是 int)数字永远不会超过 101 位。将 12 位数字乘以 3 位数字永远不会产生超过 15 位数字(添加数字计数)。

    2/ 为了提高速度,仅在绝对必要时才对数字进行规范化(减少所需的存储空间) - 我的库将此作为单独的调用,因此用户可以在速度和存储问题之间做出决定。

    3/ 正负数相加是减法,负数减法等于等值正数相加。调整符号后让add和sub方法相互调用可以节省不少代码。

    4/ 避免从小数中减去大数,因为您总是会得到如下数字:

             10
             11-
    -- -- -- --
    99 99 99 99 (and you still have a borrow).
    

    相反,从 11 中减去 10,然后取反:

    11
    10-
    --
     1 (then negate to get -1).
    

    以下是我必须为其执行此操作的其中一个库中的 cmets(已转换为文本)。不幸的是,代码本身是受版权保护的,但您可能能够挑选出足够的信息来处理这四个基本操作。下面假设-a-b代表负数,ab是零或正数。

    对于加法,如果符号不同,使用减法:

    -a +  b becomes b - a
     a + -b becomes a - b
    

    对于减法,如果符号不同,使用否定加法:

     a - -b becomes   a + b
    -a -  b becomes -(a + b)
    

    还有特殊处理以确保我们从大数中减去小数:

    small - big becomes -(big - small)
    

    乘法使用如下入门级数学:

    475(a) x 32(b) = 475 x (30 + 2)
                   = 475 x 30 + 475 x 2
                   = 4750 x 3 + 475 x 2
                   = 4750 + 4750 + 4750 + 475 + 475
    

    实现这一点的方法是一次提取 32 个数字中的每一个(向后),然后使用 add 计算要添加到结果中的值(最初为零)。

    ShiftLeftShiftRight 运算用于快速将LongInt 乘以或除以换行值(10 表示“真实”数学)。在上面的例子中,我们将 475 加到 0 2 次(32 的最后一位)得到 950(结果 = 0 + 950 = 950)。

    然后我们左移 475 得到 4750,右移 32 得到 3。将 4750 加到零 3 次得到 14250,然后将结果加到 950 得到 15200。

    左移 4750 得到 47500,右移 3 得到 0。由于右移 32 现在为零,我们完成了,实际上 475 x 32 确实等于 15200。

    除法 也很棘手,但基于早期的算术(“进入”的“gazinta”方法)。考虑12345 / 27 的以下长除法:

           457
       +-------
    27 | 12345    27 is larger than 1 or 12 so we first use 123.
         108      27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
         ---
          154     Bring down 4.
          135     27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
          ---
           195    Bring down 5.
           189    27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
           ---
             6    Nothing more to bring down, so stop.
    

    因此12345 / 27457,余数为6。验证:

      457 x 27 + 6
    = 12339    + 6
    = 12345
    

    这是通过使用下拉变量(最初为零)将 12345 的段一次拉低一个直到大于或等于 27 来实现的。

    然后我们简单地从中减去 27 直到低于 27 - 减去的次数是添加到顶行的段。

    当没有更多的细分需要降低时,我们就有了结果。


    请记住,这些是非常基本的算法。如果你的数字特别大,有更好的方法来做复杂的算术。您可以查看 GNU Multiple Precision Arithmetic Library 之类的内容 - 它比我自己的库更好更快。

    它确实有一个相当不幸的错误功能,即如果内存不足,它就会退出(在我看来,对于通用库来说,这是一个相当致命的缺陷)但是,如果你能看过去,它非常擅长什么确实如此。

    如果您出于许可原因无法使用它(或者因为您不希望应用程序无缘无故退出),您至少可以从那里获取算法以集成到您自己的代码中。

    我还发现MPIR(GMP 的一个分支)的机构更愿意讨论潜在的变化 - 他们似乎对开发人员更友好。

    【讨论】:

    • 我认为您已经很好地涵盖了“我只是想知道是否有人已经或可以提供非常详细、简单的任意精度算术解释”
    • 一个后续问题:是否可以在不访问机器码的情况下设置/检测进位和溢出?
    【解决方案2】:

    虽然重新发明轮子对您的个人启蒙和学习非常有帮助,但它也是一项艰巨的任务。我不想劝阻你,因为它是一项重要的练习,也是我自己完成的,但你应该知道,较大的软件包可以解决一些微妙而复杂的问题。

    例如,乘法。天真地,你可能会想到“小学生”的方法,即把一个数字写在另一个上面,然后像你在学校学到的那样做长乘法。示例:

          123
        x  34
        -----
          492
    +    3690
    ---------
         4182
    

    但是这种方法非常慢(O(n^2),n 是位数)。相反,现代 bignum 包使用离散傅立叶变换或数值变换将其转换为本质上为 O(n ln(n)) 的运算。

    这仅适用于整数。当您在某种类型的真实数字表示(log、sqrt、exp 等)上使用更复杂的函数时,事情会变得更加复杂。

    如果您想了解一些理论背景,我强烈建议您阅读 Yap 的书的第一章,"Fundamental Problems of Algorithmic Algebra"。如前所述,gmp bignum 库是一个优秀的库。对于实数,我使用了MPFR 并喜欢它。

    【讨论】:

    • 我对“使用离散傅里叶变换或数值变换将其转换为本质上的 O(n ln(n)) 运算”部分感兴趣——它是如何工作的?只是一个参考就可以了:)
    • @detly:多项式乘法与卷积相同,应该很容易找到有关使用 FFT 执行快速卷积的信息。任何数字系统都是多项式,其中数字是系数,底是底。当然,您需要注意进位以避免超出数字范围。
    【解决方案3】:

    不要重新发明轮子:结果可能是方形的!

    使用经过试验和测试的第三方库,例如 GNU MP

    【讨论】:

    • 如果你想学 C,我会把你的目光放低一点。实现一个 bignum 库并非易事,因为各种微妙的原因会绊倒学习者
    • 第三方库:同意,但 GMP 存在许可问题(LGPL,虽然它实际上充当 GPL,因为通过 LGPL 兼容的接口进行高性能数学有点困难)。
    • 不错的 Futurama 参考(故意的?)
    • GNU MP 在分配失败时无条件地调用abort(),这必然会发生在某些非常大的计算中。这对于库来说是不可接受的行为,并且有足够的理由编写您自己的任意精度代码。
    • 我必须同意 R 的观点。一个通用库在内存不足时简单地从程序下面拉出地毯是不可原谅的。我宁愿他们为了安全/可恢复性而牺牲一些速度。
    【解决方案4】:

    你做这件事的方式基本上和你用铅笔和纸做的一样......

    • 数字将在一个缓冲区(数组)中表示,该缓冲区可以根据需要采用任意大小(这意味着使用mallocrealloc
    • 您尽可能使用语言支持的结构实现基本算术,并手动处理进位和移动小数点
    • 您搜索数值分析文本以找到更复杂的函数处理的有效参数
    • 您只需根据需要实施即可。

    通常你会使用你的基本计算单位

    • 包含 0-99 或 0-255 的字节
    • 16位字包含0-9999或0--65536
    • 32 位字包含...
    • ...

    由您的架构决定。

    二进制或十进制基数的选择取决于您对最大空间效率、人类可读性以及芯片上是否存在二进制编码十进制 (BCD) 数学支持的期望。

    【讨论】:

      【解决方案5】:

      你可以用高中数学水平做到这一点。尽管在现实中使用了更高级的算法。因此,例如添加两个 1024 字节的数字:

      unsigned char first[1024], second[1024], result[1025];
      unsigned char carry = 0;
      unsigned int  sum   = 0;
      
      for(size_t i = 0; i < 1024; i++)
      {
          sum = first[i] + second[i] + carry;
          carry = sum - 255;
      }
      

      结果必须大one place,以防加法处理最大值。看看这个:

      9
         +
      9
      ----
      18
      

      TTMath 是一个很棒的图书馆,如果你想学习的话。它是使用 C++ 构建的。上面的例子很傻,但加法和减法一般都是这样完成的!

      关于这个主题的一个很好的参考是Computational complexity of mathematical operations。它告诉您要实现的每个操作需要多少空间。例如,如果您有两个N-digit 数字,那么您需要2N digits 来存储相乘的结果。

      正如 Mitch 所说,实现起来远不是一件容易的事!如果你懂 C++,我建议你看看 TTMath。

      【讨论】:

      • 我确实想到了数组的使用,但我正在寻找更通用的东西。感谢您的回复!
      • 嗯……提问者的名字和图书馆的名字不会是巧合吧? ;)
      • 大声笑,我没注意到!我真希望 TTMath 是我的 :) 顺便说一句,这是我关于这个主题的一个问题:
      【解决方案6】:

      最终参考资料之一(恕我直言)是 Knuth 的 TAOCP 第 II 卷。它解释了许多用于表示数字的算法以及对这些表示的算术运算。

      @Book{Knuth:taocp:2,
         author    = {Knuth, Donald E.},
         title     = {The Art of Computer Programming},
         volume    = {2: Seminumerical Algorithms, second edition},
         year      = {1981},
         publisher = {\Range{Addison}{Wesley}},
         isbn      = {0-201-03822-6},
      }
      

      【讨论】:

        【解决方案7】:

        假设您希望自己编写一个大整数代码,这可能非常简单,就像最近做过的人一样(虽然是在 MATLAB 中)。以下是我使用的一些技巧:

        • 我将每个单独的十进制数字存储为双精度数。这使得许多操作变得简单,尤其是输出。虽然它确实占用了比您希望的更多的存储空间,但这里的内存很便宜,如果您可以有效地对一对向量进行卷积,它会使乘法非常有效。或者,您可以将多个十进制数字存储在双精度数中,但要注意进行乘法运算的卷积可能会导致非常大的数字出现数值问题。

        • 单独存储一个符号位。

        • 两个数字相加主要是数字相加,然后在每一步检查进位。

        • 一对数字的乘法最好在卷积之后进行进位步骤,至少如果您有一个快速卷积码可用。

        • 即使您将数字存储为单个十进制数字的字符串,也可以进行除法(也称为 mod/rem ops)以在结果中一次获得大约 13 个十进制数字。这比一次只处理 1 个十进制数字的除法效率高得多。

        • 要计算整数的整数幂,请计算指数的二进制表示。然后根据需要使用重复的平方运算来计算幂。

        • 许多操作(分解、素性测试等)都将受益于 powermod 操作。也就是说,当您计算 mod(a^p,N) 时,在取幂的每个步骤中减少结果 mod N,其中 p 以二进制形式表示。不要先计算 a^p,然后尝试减少它 mod N。

        【讨论】:

        • 如果您存储的是单个数字而不是 base-10^9 或 base-2^32 或类似的东西,那么您所有花哨的卷积乘法都只是浪费。当你的常数是 that 不好时,Big-O 毫无意义......
        【解决方案8】:

        这是我在 PHP 中做的一个简单(天真的)示例。

        我实现了“加法”和“乘法”并将其用于指数示例。

        http://adevsoft.com/simple-php-arbitrary-precision-integer-big-num-example/

        代码片段

        // Add two big integers
        function ba($a, $b)
        {
            if( $a === "0" ) return $b;
            else if( $b === "0") return $a;
        
            $aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9);
            $bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9);
            $rr = Array();
        
            $maxC = max(Array(count($aa), count($bb)));
            $aa = array_pad(array_map("strrev", $aa),$maxC+1,"0");
            $bb = array_pad(array_map("strrev", $bb),$maxC+1,"0");
        
            for( $i=0; $i<=$maxC; $i++ )
            {
                $t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT);
        
                if( strlen($t) > 9 )
                {
                    $aa[$i+1] = ba($aa[$i+1], substr($t,0,1));
                    $t = substr($t, 1);
                }
        
                array_unshift($rr, $t);
             }
        
             return implode($rr);
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-02-09
          • 2011-02-21
          • 2015-10-29
          相关资源
          最近更新 更多