Xiangism

从一个无知角落里开始,蹒跚学步,一个未知到另一个未知,在跌跌撞撞中越走越快,越走越远,最后宇宙也为之开源。对于探索者来说,最后他们的思想总是变得和自己的足迹一样伟大。
  博客园  :: 首页  :: 联系 :: 管理

支持无限精度无限大数的类BigNumber实现

Posted on 2015-07-03 23:17  Xiangism  阅读(13436)  评论(10编辑  收藏  举报

介绍

本篇是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

其实就是一个很简单的状态机,首先看首字符是否为 - +

  1. 如果为-,那么这个数为负数,跳到4
  2. 如果为+, 那么这个数为正数,跳到4
  3. 如果为数字,那么这个数为正数,跳到4
  4. 扫描所有数字直到遇到 . 将扫描到的所有数字分组后存储在IntPart中
  5. 扫描所有数字直到结尾,将扫描到的所有数字分组后存储在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) 的字符串程序即可得到结果。

敬请期待~~

 

转载本博客上的原创文章者,请注明出处