只使用加法和乘法实现除法
本文关键字:实现 除法 | 更新日期: 2023-09-27 18:29:35
我正在用C#编写一个BigDecimal类。我已经成功地实现了+、-和*运算符。但我想不出一种方法来计算2个大小数的除法。使用这3个运算符实现除法的最快方法是什么?或者有更好的方法吗?(同时考虑开发时间和算法速度)
目标1:我希望结果是另一个具有固定精度(应该是可变的)的BigDecimal
目标2:正如您所提到的,BigDecimal的目的不是固定的精度。那么,如何实现无限精度?
另一个问题:使用Microsoft BCL的BigRational
类进行任意精度运算,然后在这个线程中使用Christopher Currens的扩展方法是否更好(关于速度和灵活性):C#中有BigFloat类吗?以获得十进制表示而不是编写一个新类?
首先,我假设"大小数"的意思是表示一个有理数,其中分母被限制为10的任意幂。
你需要认真思考两个小数除法的输出是什么。小数在加法、减法和乘法上是闭合的,但在除法上不是闭合的。也就是说,任何两个小数相乘都会产生第三个小数:(7/10)*(9/100)得出63/1000,这是另一个小数。但是除以这两个小数,你得到一个分母中没有10次方的有理数。
为了回答你实际提出的问题:就像乘法可以由循环中的加法构成一样,除法也可以由循环的减法构成。要将23除以7,请说:
- 是7<23?是的
- 23减去7得到16
- 是7<16?是的
- 16减去7得到9
- 是7<9?是的
- 9减去7得到2
- 是7<2.没有。我们有第一个数字。我们做了三次减法,所以第一个数字是3
- 2乘以10得到20
- 是7<20?是的
- 20减去7得到13
- 是7<13?是的
- 15减去7得到6
- 是7<6.没有。我们做了两次减法和一次10的乘法运算,所以下一个数字是2
- 6乘以10得到60
- 是7<60?是的
- 我们做了八次减法,所以下一个数字是8
- 。。。等等
你知道有什么更快的算法吗?
当然,有很多更快的除法算法。这里有一个:Goldschmidt算法。
首先,我希望很清楚,如果你试图计算X / D
,那么你可以先计算1 / D
,然后乘以X
。此外,让我们假设WOLOG,D严格地在0和1之间。
如果不是呢?如果D为负数,则将其和X反转;如果D为零,则给出一个错误;如果D是1,则答案是X;如果D大于1,那么将它和X都除以10,这对你来说应该很容易,因为你的系统是十进制的。继续应用这些规则,直到你的D介于0和1之间。(作为额外的优化:当D很小时,算法是最慢的,所以如果D小于,比如说0.1,将X和D乘以10,直到D大于或等于0.1。)
好的,我们的问题是,我们有一个介于0和1之间的数字D,我们希望计算1 / D
。也许最简单的事情就是做一个例子。假设我们正在尝试计算CCD_ 6。正确答案是1.42857142857...
从2减去0.7得到1.3。现在将分数的两部分乘以1.3:
(1 / 0.7) * (1.3 / 1.3) = 1.3 / 0.91
太好了。我们现在已经将1 / 0.7
计算到一位数的准确度。
现在再做一次。2减去0.91得到1.09。将分数的两部分乘以1.09:
(1.3 / 0.91) * (1.09 / 1.09) = 1.417 / 0.9919
太好了,现在我们有两个正确的数字。现在再做一次。2减去0.9919得到1.0081。顶部和底部乘以1.0081:
(1.417 / 0.9919) * (1.0081 / 1.0081) = 1.4284777 / 0.99993439
嘿,现在我们有四个正确的数字。看看进展如何?每走一步,分母都会更接近1,因此分子也会更接近1 / 0.7
。
这比减法收敛得快得多。
你明白为什么它有效吗?
由于D在0和1之间,所以存在一个数E,使得D=1-E,并且E也在0和一之间。
当我们把D乘以(2-D)时,我们就是把(1-E)乘以(1+E),这就得到1-E2。
由于0<E<1,显然E2小于E,也在0和1之间,这意味着1-E2更接近1。事实上,它是一个批次,更接近1。通过多次重复这个过程,我们很快就会接近1。实际上,我们在这里所做的是将每一步的正确数字数量大致增加一倍。显然,这比减法要好得多,减法在每一步上给一个额外的数字。
继续这样做,直到你达到你想要的准确性。由于每一步的准确数字数量大约是原来的两倍,因此您应该能够很快达到可接受的准确度。由于我们已经安排D从一开始就大于或等于0.1,因此E永远不会大于0.9;反复求0.9的平方可以很快地把你降到一个很小的数字。
- 您可以在Java BigDecimal实现中找到一些想法
- 对于计算a/b,您可以使用二进制搜索算法来查找这样的c,即b*c=a(您应该运行算法,直到达到所需的精度)
- 你也可以看看这个BigFloat类,它有一个有趣的指数形式的实现
- 对于无限精度,您可以将
BigDecimal
存储为两个BigInteger
的有理分式
有理分式表示的代数:
(x1/x2) + (y1/y2) = (x1*y2+x2*y1)/(x2*y2)
(x1/x2) - (y1/y2) = (x1*y2-x2*y1)/(x2*y2)
(x1/x2) * (y1/y2) = (x1*y1)/(x2*y2)
(x1/x2) / (y1/y2) = (x1*y2)/(x2*y1)
双精度(固定精度)实现示例:
public double Divide(double a, double b, double eps)
{
double l = 0, r = a;
while (r - l > eps)
{
double m = (l + r) / 2;
if (m * b < a)
l = m;
else
r = m;
}
return (l + r) / 2;
}
Eric Lippert的答案看起来很像"以最慢的方式进行长除法"。它是准确的,但不快。假设您有一种用双精度近似BigDecimal的方法,并且由于您已经在BD
类中实现了+、-和*,您可以(近似地)执行以下操作:
BD operator/(BD a, BD b) {
BD r, result=0, residual;
double aa, bb, rr;
residual = a;
bb = b;
while (residual > desired) {
rr = (double)residual / bb;
r = rr; // assuming you have a conversion from double to bigdecimal with loss of precision
result += r;
residual = a - (result * b);
}
return result;
}
这将进行逐次逼近,但同时需要多个数字(基本上,您可以使用BD算术找到错误,然后使用二重算术将其除法)。我认为这应该是一个相对直接和有效的方法。
我已经使用float
和double
实现了上面的一个版本——只是为了向自己证明这个原理是可行的。使用简单的C框架,只需要两次迭代就可以将精度降低到double
除法的水平。我希望你明白。
#include <stdio.h>
double divide(double a, double b);
int main(void) {
double a, b, result;
float fa, fb, fr;
a = 123.5;
b = 234.6;
fa = a;
fb = b;
fr = fa / fb;
printf("using float: %f'n", fr);
result = divide(a, b);
printf("using double: %lf'n", result);
printf("difference: %le'n", result - fr);
}
double divide(double a, double b) {
double r, result=0, residual;
float aa, bb, rr;
residual = a;
bb = b;
while (residual > 1e-8) {
rr = (float)residual / bb;
r = rr; // assuming you have a conversion from double to bigdecimal with loss of precision
result += r;
residual = a - (result * b);
printf("residual is now %le'n", residual);
}
return result;
}
输出:
using float: 0.526428
residual is now 8.881092e-06
residual is now 5.684342e-14
using double: 0.526428
difference: 3.785632e-08