任意精度算术说明


91

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

我只想知道是否有人可以或可以提供关于任意精度算术的非常详细,愚蠢的解释。

Answers:


162

足够的存储空间和算法可以将数字视为较小的部分。假设您有一个编译器,其中an int只能是0到99,并且您想处理的数字最大为999999(为简化起见,我们只担心正数)。

您可以通过给每个数字赋予3 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位int限制为0到99平方时生成9,801(<32,768),或者int使用0到9,999生成32位以生成99,980,001(<2,147,483,648),这大大简化了算法。

注意一些技巧。

1 /在增加或增加数字时,请预先分配所需的最大空间,然后在发现过多空间时减少。例如,将两个100-“数字”(其中的数字是int)相加将永远不会给您超过101个数字。将12位数字乘以3位数字将不会产生超过15位的数字(加上数字计数)。

2 /为了提高速度,仅在绝对必要时才对数字进行归一化(减少所需的存储空间)-我的库将其作为单独的调用,以便用户可以在速度和存储空间之间做出选择。

3 /正负数的加法为减法,负数的减法与等价的正数相同。通过在调整符号后使add和减法相互调用,可以节省大量代码。

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

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

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

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

这是我必须为此做的其中一个库的注释(变成了文本)。不幸的是,该代码本身已受版权保护,但是您可以选择足够的信息来处理这四个基本操作。下面假定-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个数字中的每个数字(向后),然后使用加法计算要添加到结果中的值(最初为零)。

ShiftLeftShiftRight运算符用于将LongInt换位值快速乘以或除以a (对于“实数”数学为10)。在上面的示例中,我们将475加到零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多精度算术库之类的东西,它比我自己的库快得多,而且更好。

它确实有一个不幸的功能,就是如果它用完了内存就会退出(我认为这对于通用库来说是一个致命的缺陷),但是,如果您能够超越它,那么它的功能就相当不错了。

如果您由于许可原因而不能使用它(或者因为不想让应用程序退出而没有明显原因),则至少可以从中获取用于集成到自己的代码中的算法。

我还发现,在在的BOD MPIR(GMP的一个分支)有更适合的潜在变化的讨论-他们似乎更开发者友好的一群。


14
我认为您涵盖了“我只想知道是否有人拥有或可以提供关于任意精度算术的非常详细,愚蠢的解释”非常好
Grant Peters 2010年

一个后续问题:是否可以在不访问机器代码的情况下设置/检测进位和溢出?
2014年

8

重新发明轮子对您的个人教育和学习非常有益,但它也是一项艰巨的任务。我不想说服您将它作为一项重要的工作,而是我自己完成的一项工作,但是您应该意识到,较大的软件包正在解决一些细微而复杂的问题。

例如,乘法。天真的,您可能会想到“小学生”方法,即在另一个数字上方写一个数字,然后在学校学习时进行长乘法。例:

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

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

这只是整数。当您使用某种数字的实数表示形式(log,sqrt,exp等)使用更复杂的功能时,事情就变得更加复杂。

如果您需要一些理论背景,我强烈建议您阅读Yap的书的第一章“算法代数的基本问题”。如前所述,gmp bignum库是一个出色的库。对于实数,我使用了mpfr并喜欢它。


1
我对“使用离散傅立叶变换或数值变换将其转换为本质上是O(n ln(n))运算”的部分感兴趣—这是如何工作的?只是参考就可以了:)
不错,2010年

1
@detly:多项式乘法与卷积相同,应该很容易找到有关使用FFT进行快速卷积的信息。任何数字系统都是一个多项式,其中的数字是系数,而底数是底数。当然,您需要保管好携带物,以免超出数字范围。
Ben Voigt 2012年

6

不要重新发明轮子:它可能变成正方形!

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


4
如果您想学习C,我会把您的眼光放低一点。实施bignum库并不是一件容易的事,因为各种各样的微妙原因都会吸引学习者的注意
Mitch Wheat

3
第三方库:同意,但是GMP存在许可问题(LGPL,尽管实际上起着GPL的作用,因为通过兼容LGPL的接口很难进行高性能数学运算)。
詹森·S,2009年

不错的未来世界参考(有意吗?)
格兰特·彼得斯2010年

7
GNU MP无条件地调用abort()分配失败,这必然会在某些疯狂的大计算中发生。对于库而言,这是不可接受的行为,并且有足够的理由编写您自己的任意精度代码。
R .. GitHub停止帮助ICE,2010年

我必须同意那里的R。当内存不足时,仅从程序下面拉出地毯的通用库是不可原谅的。我宁愿他们为安全性/可恢复性牺牲一些速度。
paxdiablo

4

基本上用铅笔和纸来做...

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

通常,您将使用基本计算单位

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

根据您的体系结构。

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


3

您可以通过高中数学来做到这一点。尽管实际上使用了更高级的算法。因此,例如添加两个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 ++构建的。上面的例子很愚蠢,但这通常是加法和减法的方法!

关于该主题的一个很好的参考是数学运算的计算复杂性。它告诉您要实现的每个操作需要多少空间。例如,如果您有两个N-digit数字,则需要2N digits存储乘法结果。

正如Mitch所说,到目前为止,实施起来并非易事!如果您了解C ++,我建议您看一下TTMath。


我确实很喜欢使用数组,但是我正在寻找更通用的东西。感谢您的回复!
TT。

2
嗯...询问者的名字和图书馆的名字不能巧合,是吗?;)
John Y 2009年

大声笑,我没注意到!我真希望TTMath是我的:)顺便说一下,这是我关于这个问题的问题之一:
AraK


3

最终参考书(IMHO)之一是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},
}

1

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

  • 我将每个单独的十进制数字存储为双精度数字。这使许多操作变得简单,尤其是输出。尽管它确实占用了过多的存储空间,但是这里的内存却很便宜,并且如果您可以高效地对向量进行卷积,那么乘法将非常有效。或者,您可以将多个十进制数存储为双精度数,但是请注意,进行乘法的卷积会在很大的数上引起数值问题。

  • 单独存储符号位。

  • 两个数字的加法主要是将数字相加,然后在每个步骤中检查是否有进位。

  • 最好将一对数字相乘作为卷积,然后再进行进位步骤,至少在使用快速卷积码的情况下如此。

  • 即使将数字存储为单个十进制数字的字符串,也可以进行除法(也称为mod / rem ops)以一次获得大约13个十进制数字。这比一次只能使用1个小数位的除法效率更高。

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

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


1
如果您存储的是单个数字而不是base-10 ^ 9或base-2 ^ 32或类似的数字,那么所有花哨的卷积乘法运算都只是浪费。当您的常数那么糟糕时,Big-O毫无意义...
R .. GitHub停止帮助ICE 2010年

0

这是我在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);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.