我正在尝试学习C,并且遇到了无法处理非常大的数字(即100位数字,1000位数字等)的问题。我知道有可以执行此操作的库,但是我想尝试自己实现。
我只想知道是否有人可以或可以提供关于任意精度算术的非常详细,愚蠢的解释。
Answers:
足够的存储空间和算法可以将数字视为较小的部分。假设您有一个编译器,其中an int
只能是0到99,并且您想处理的数字最大为999999(为简化起见,我们只担心正数)。
您可以通过给每个数字赋予3 int
s并使用您在小学应该(应该已经)学到的相同规则进行加,减和其他基本运算来做到这一点。
在一个任意精度的库中,用于表示我们的数字的基本类型的数量没有固定的限制,无论内存可以容纳多少。
例如123456 + 78
:
12 34 56
78
-- -- --
12 35 34
从最低端开始工作:
实际上,这就是加法通常在CPU内部的位级别上如何工作。
减法是相似的(使用基本类型的减法并借位而不是进位),可以通过重复加法(非常慢)或叉积(更快)来进行乘法,除法比较棘手,但是可以通过对数字进行移位和减法来完成参与(您从小就学到的漫长的分裂)。
我实际上已经编写了库来使用最大的10的幂来执行此类操作,平方的平方可以将其乘以整数(以防止在将两个int
s 相乘时发生溢出,例如将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
表示负数,并且a
和b
为零或正数。
对于另外,如果标志是不同的,否定的使用减法:
-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个数字中的每个数字(向后),然后使用加法计算要添加到结果中的值(最初为零)。
ShiftLeft
和ShiftRight
运算符用于将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 / 27
是457
有剩余6
。校验:
457 x 27 + 6
= 12339 + 6
= 12345
这是通过使用下拉变量(最初为零)来实现的,一次将12345的段降低一个,直到大于或等于27。
然后,我们简单地从中减去27,直到降至27以下-相减的次数就是添加到最上面一行的部分。
当没有更多细分可放时,我们得到了结果。
请记住,这些是非常基本的算法。如果您的数字特别大,则有更好的方法来执行复杂的算术运算。您可以研究GNU多精度算术库之类的东西,它比我自己的库快得多,而且更好。
它确实有一个不幸的功能,就是如果它用完了内存就会退出(我认为这对于通用库来说是一个致命的缺陷),但是,如果您能够超越它,那么它的功能就相当不错了。
如果您由于许可原因而不能使用它(或者因为不想让应用程序退出而没有明显原因),则至少可以从中获取用于集成到自己的代码中的算法。
我还发现,在在的BOD MPIR(GMP的一个分支)有更适合的潜在变化的讨论-他们似乎更开发者友好的一群。
重新发明轮子对您的个人教育和学习非常有益,但它也是一项艰巨的任务。我不想说服您将它作为一项重要的工作,而是我自己完成的一项工作,但是您应该意识到,较大的软件包正在解决一些细微而复杂的问题。
例如,乘法。天真的,您可能会想到“小学生”方法,即在另一个数字上方写一个数字,然后在学校学习时进行长乘法。例:
123
x 34
-----
492
+ 3690
---------
4182
但是此方法非常慢(O(n ^ 2),n为位数)。取而代之的是,现代bignum包使用离散傅立叶变换或数值变换将其转换为本质上为O(n ln(n))的运算。
这只是整数。当您使用某种数字的实数表示形式(log,sqrt,exp等)使用更复杂的功能时,事情就变得更加复杂。
如果您需要一些理论背景,我强烈建议您阅读Yap的书的第一章“算法代数的基本问题”。如前所述,gmp bignum库是一个出色的库。对于实数,我使用了mpfr并喜欢它。
不要重新发明轮子:它可能变成正方形!
使用经过尝试和测试的第三方库,例如GNU MP。
abort()
分配失败,这必然会在某些疯狂的大计算中发生。对于库而言,这是不可接受的行为,并且有足够的理由编写您自己的任意精度代码。
基本上用铅笔和纸来做...
malloc
和realloc
)通常,您将使用基本计算单位
根据您的体系结构。
选择二进制或十进制基数取决于您对最大空间效率,人类可读性以及芯片上是否缺少二进制编码十进制(BCD)数学支持的期望。
您可以通过高中数学来做到这一点。尽管实际上使用了更高级的算法。因此,例如添加两个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。
最终参考书(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},
}
假设您希望自己编写一个大的整数代码,那么做起来可能非常简单,就像最近做过这个的人所说的(尽管在MATLAB中)。以下是我使用的一些技巧:
我将每个单独的十进制数字存储为双精度数字。这使许多操作变得简单,尤其是输出。尽管它确实占用了过多的存储空间,但是这里的内存却很便宜,并且如果您可以高效地对向量进行卷积,那么乘法将非常有效。或者,您可以将多个十进制数存储为双精度数,但是请注意,进行乘法的卷积会在很大的数上引起数值问题。
单独存储符号位。
两个数字的加法主要是将数字相加,然后在每个步骤中检查是否有进位。
最好将一对数字相乘作为卷积,然后再进行进位步骤,至少在使用快速卷积码的情况下如此。
即使将数字存储为单个十进制数字的字符串,也可以进行除法(也称为mod / rem ops)以一次获得大约13个十进制数字。这比一次只能使用1个小数位的除法效率更高。
要计算整数的整数幂,请计算指数的二进制表示形式。然后,根据需要使用重复的平方运算来计算功效。
powermod操作将使许多操作(分解,素性测试等)受益。也就是说,当您计算mod(a ^ p,N)时,请在以p表示为二进制形式的幂运算的每个步骤中减少结果mod N。不要先计算a ^ p,然后尝试将其降低为modN。
这是我在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);
}