介绍
本篇是MathAssist的第二篇,在前言中粗略地展示了MathAssist的“计算和证明”能力,本篇开始将详细介绍其实现原理。 从计算开始说起,要实现任意大数的计算器首先得有一个类支持大数运算,于是本篇介绍BigNumber的实现。
一般编程语言提供的数字类型都是基于cpu位数来实现,这样做是为了在基础类型上保证运算速度。 想当年本人刚开始学vb6(也是刚开始学程序)时,
想用这个圆周率公式来精确到小数点后上万位,可结果好像是在小数点后7、8位就无法再精确了。 稍微想下就可明白原因——所使用的float类型本身就只提供小数点后几位的精确度,而用它所计算出来的结果怎么可能精确到很多呢?
既然编程语言提供的类型无法实现无限精度,那么可以自定义一个类型来实现之。 其关键思想就是用数组来存储大数每位上的数,比如用List<int> (这里不严谨地将Lite<T>称为数组)。比如123456789这个数,可以将其放在数组中,数组元素分别为1,2,3,4,5,6,7,8,9。 然后再实现算法来完成加、减、乘、除运算即可。
BigNumber概要
BigNumber的属性很简单:
- List<int> IntPart; //整数部分
- List<int> DecimalPart; //小数部分
- bool IsPlus; //是否是正数
- BigNumber AbsoluteNumber; //返回绝对值
- BigNumber ReverseNumber; //返回相反数
上面说过可以在数组元素中每个元素存储一位,不过细想一下,数组元素类型是int,最大是2147483647,可以存储多位数。这样存储后不仅可以大大节约程序的内存空间,也节约提高了运算速度。
故提供一个静态只读变量 int OneCount 来表示一个数组元素中存储数的位数。为了方便调试现在暂时设置为 4。
看一个简单的例子:
如果用如下的代码实例化一个BigNumber
BigNumber num1 = new BigNumber("13579.02468");
那么其IntPart为1, 3579,其DecimalPart为246, 8000。
也就是说从小数点开始,整数部分从右往左每4个一组,小数部分从左往右每4个一组。上面的246其实应该是0246,而用int表示的0246和246是一样的。而8000不能用8表示,如果是8的话,那么表示的就是13579.02460008这个数了。
BigNumber还实现了接口IComparable,这样用CompareTo()即可比较两个BigNumber的大小。
从字符串中识别出BigNumber
其实就是一个很简单的状态机,首先看首字符是否为 - +
- 如果为-,那么这个数为负数,跳到4
- 如果为+, 那么这个数为正数,跳到4
- 如果为数字,那么这个数为正数,跳到4
- 扫描所有数字直到遇到 . 将扫描到的所有数字分组后存储在IntPart中
- 扫描所有数字直到结尾,将扫描到的所有数字分组后存储在DeciamlPart中
具体代码在IdentifyNumber.cs中.
注意不支持首字符是小数点的写法,即".14159"是非法格式。
加、减、乘、除的实现
加、减、乘、除的实现本质上就是模拟人工用笔算,考查的是对for, if和数组的驾驶能力。下面将讲解核心点以及需要注意的地方,具体的代码细节不在此赘述,有兴趣的朋友可以再交流。后面提供源码,以供参考。
加、减法的实现
考虑到正、负号,加减法各有4种情况,分别如下
- 正数加正数、正数加负数、负数加正数、负数加负数
- 正数减正数、正数减负数、负数减正数、负数减负数
而减法又要考虑两数的大小。
不过用绝对值和相反数进行变换后,上面的情况其实可以划归为两种:正数加正数、较大数减较小数。
比如负数加负数,可以先将两负数取绝对值后变成正数加正数,得到的结果指定为负数即可;正数减负数,其实就是正数加正数。具体代码在BigCalculate.Add(), BigCalculate.Minus()中。
而正数加正数、较大数减较小数,先计算小数部分、再计算整数部分,注意进位、对齐等问题即可。现在一个数组元素中存储一个四位数,那个当和为10000时才要需要进位。代码在BigCalculate.PlusAdd(), BigCalculate.PLusMinus()中。
乘法的实现
乘法在笔算中会先忽略小数点,所以先要将BigNumber转化为List<int>,然后对两个List<int>进行相乘。BigCalculate.Multiply(List<int> one, List<int> two)函数实现。
具体的算法就是在双重for循环中依次对两个数组中的元素进行相乘,注意进位的问题即可。
除法的实现
除法由于可以无限的试商、无限除,所以默认情况下会取除数、被除数最大的精度为结果的精度。具体代码在BigCalculate.Division(List<int> one, List<int> two)
如下看一下四则运算的实际运行结果
static void TestBaseCal1() { BigNumber num1 = new BigNumber("112233445566778899.02468"); BigNumber num2 = new BigNumber("123456789.13579"); BigNumber r1 = num1 + num2; BigNumber r2 = num1 - num2; BigNumber r3 = num1 * num2; BigNumber r4 = num1 / num2; Console.WriteLine(num1.ToString() + " + " + num2.ToString() + " = " + r1.ToString()); Console.WriteLine(num1.ToString() + " - " + num2.ToString() + " = " + r2.ToString()); Console.WriteLine(num1.ToString() + " * " + num2.ToString() + " = " + r3.ToString()); Console.WriteLine(num1.ToString() + " / " + num2.ToString() + " = " + r4.ToString()); }
将生成如下的结果
112233445566778899.02468000 + 123456789.13579000 = 112233445690235688.16047000 112233445566778899.02468000 - 123456789.13579000 = 112233445443322109.88889000 112233445566778899.02468000 * 123456789.13579000 = 13855980823320987520055131.2510812972000000 112233445566778899.02468000 / 123456789.13579000 = 909090916.36372823
分数次幂运算的实现
因为
ca.b=ca* c0.b
上面的a.b表示123.456这样的小数,其中a=123,b=456。也就是说,分数可分成整数部分和小数部分,整数次幂即对数进行整数次自乘,所以关键是实现(0,1)之间的小数次幂,而开平方运算是小数次幂的基础。
开平方运算
先讲解如何笔算来实现开方。
以13开方为例,如图是计算的过程:
先定义几个变量: n表示表示要计算的数,x表示一次尝试时的商,d表示余数,r表示计算结果。
其中r=36055;n第一次是13,第二次是400;x是每次计算时的3,6,0,5,5;d是4,4
下面我们一步步来看如何进行开方运算:
第一步直接试x*x < n,可以得到x=4,余数d=n-x*x=4
r | 20r | n | x | d |
0 | 0 | 13 | 3 | 4 |
然后将x=3添加到r中,将d=4后面加两个0赋值给n,得到如下
r | 20r | n | x | d |
3 | 60 | 400 |
这时用(20r+x)*x<n,来尝试新的x,于是得到x=6,用d=n-(20r+x)*x来算d,得到d=4,于是
r | 20r | n | x | d |
3 | 60 | 400 | 6 | 4 |
将x=6添加到r,得r=36,将d=4后面加两个0赋值给n,得到如下
r | 20r | n | x | d |
36 | 720 | 400 |
如此循环,即可达到任意精度,最后再在适当位置加上小数点即可。
下面简单讨论下上面这样做的原理。上面第一次试商后,可以列出下面的等式,
(10r+x)2=100r2+(20r+x)*x
现在也就是要求最大的x,所以就必须要满足 (20r+x)*x < n,使x的值尽可能的大。
如果数组元素中一次存储4数,那么上面的等式应该是
(1000r+x)2=1000000r2+(2000r+x)*x
所以试商时就应该使(2000r+x)*x最大化。
还是那句话,详细代码、每个for, if在什么情况下跳转不在此具体细说,代码在DecimalPowerCalculator.Sqrt();
小数次幂的实现
开方运算实际上就是1/2次幂,将其结果再开方就可得1/4次幂,1/8,1/16,1/32次幂……
现在要做的就是用 1/2n这样的数来表示(0,1)之间的任意数。
先看一个引子:根据等比数列求和公式,
1/2, 1/4, 1/8, 1/16,所有这样的数相加结果等于1。因为a1=1/2, q=1/2,n无穷大时q^n=0,所以sn=1
既然所有的1/2n的和为1,那么当要表示一个小于1的数时,是不是就可以从1/2n中取出部分数来表示??
下面即将来到本篇最精彩的部分~~~~
想到这个办法,本人是从这个例子《二进制数的妙用》中得到的启发,也就是在二进制数的帮助下实现。整数可以表示成二进制,小数同时也有对应的二进制表示。
比如0.3的二进制就是 (0.0[1001])2,其中中括号中表示循环,也就0.01001100110011001....
而二进制的0.1对应十进制的1/2,
二进制的0.01对应1/4,
二进制的0.001对应1/8
……
如此,只要将小数表示成二进制,然后取对应位上有1的部分表示成1/2n即可。
代码在DeciamlPowerCalculator.Power(BigNumber value, BigNumber pow, int precision)中。
小数二进制的转化,就是不断乘2的过程,如果结果大于1,那么就增加一个1,在这里就是要执行一个开2n次的运算。这就是while(true) {}循环所做的事。
示例代码
1 static void TestSqrt() 2 { 3 BigNumber num1 = new BigNumber("2.1"); 4 BigNumber r1 = num1.Power(new BigNumber("0.5"), 30); 5 6 BigNumber a1 = new BigNumber("2.1"); 7 BigNumber a2 = new BigNumber("3.2"); 8 BigNumber r2 = a1.Power(a2); 9 10 Console.WriteLine("sqrt " + num1.ToString() + " = " + r1.ToString()); 11 Console.WriteLine("pow " + a1.ToString() + ", " + a2.ToString() + " = " + r2.ToString()); 12 13 }
程序输出
sqrt 2.1000 = 1.4491376746189438573718664157169771723140 pow 2.1000, 3.2000 = 10.74241038
用windows自带计算器计算2.13.2 结果为 10.742410477394706348894189127936
可以看出BigNumber所计算出来的结果有偏差。因为上面十进制到二进制的转化是一个无限的过程,计算时指定的精度越高,结果才会越精确。如果用下面的代码
BigNumber r2 = a1.Power(a2, 20);
将会得到这个值 10.74241047739470634889418906995290312487247243501184203826786422147158331781971904
可以发现更精确了。当指定更高精度时
BigNumber r2 = a1.Power(a2, 50);
在等待几秒种后,得到下面的结果,可以发现与windows计算器的结果一样了。
10.74241047739470634889418912793594027312904293501813528013 44316317300323173144011738455572486083328381540406651346840698674394856848366339 1163623786324109576722452826633389343718013335156981992425148434
不足之处
如果上面所示,BigNumber.Power()第二个参数所指定的精度是结果小数点所保留的长度,其显示出来的后面的值是不完全正确的,可以进行一个判断,对结果进行截取,只取正确的数值。不过本人不想再捣腾这些代码了,于是就这样吧。
sin, cos, exp的实现
有了上面的四则运算和幂运算后,实现sin,cos,exp就非常容易了。关键是四个字:泰勒公式。
由于过了四年,本人还是看代码才想起来是这样实现。现在看来当时这么做完全是想表示下自己学过高等数学。
公式如下:
只要n取得足够大,就可以满足任意所需的精度。具体代码在TaylorFunction.cs
示例代码
static void TestTay() { BigNumber num1 = new BigNumber("3.14159265358939"); BigNumber s = TaylorFunction.Sine(num1, 10); BigNumber c = TaylorFunction.Cosine(num1, 10); BigNumber ePi = TaylorFunction.Exp(num1, 10); Console.WriteLine("sin " + num1.ToString() + " = " + s.ToString()); Console.WriteLine("cos " + num1.ToString() + " = " + c.ToString()); Console.WriteLine("exp " + num1.ToString() + " = " + ePi.ToString()); }
程序输出
sin 3.1415926535893900 = 0.0000000000004032384642235390642455540857 cos 3.1415926535893900 = -0.9999999999999999999999258414185768854889 exp 3.1415926535893900 = 23.1406926327699377884050942516470550152418
至此BigNumber介绍完毕。下面提供下载路径:
exe程序BigNumberExe
源码项目 BigNumber
接下来的文章将介绍在BigNumber的基础上实现的计算器程序,到时可直接输入形式 234.23423+exp(PI) 的字符串程序即可得到结果。
敬请期待~~
转载本博客上的原创文章者,请注明出处