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


    介绍

    本篇是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) 的字符串程序即可得到结果。

    敬请期待~~

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

  • 相关阅读:
    引号的区别
    QT中加载动态链接库
    QString 转换为 char *
    C++虚继承初识
    虚析构函数详解
    赋值兼容规则
    利用docker搭建spark hadoop workbench
    《用Java写一个通用的服务器程序》03 处理新socket
    《用Java写一个通用的服务器程序》02 监听器
    《用Java写一个通用的服务器程序》01 综述
  • 原文地址:https://www.cnblogs.com/xiangism/p/4614674.html
Copyright © 2020-2023  润新知