C#实现任意大数的计算和简单逻辑命题的证明——BigNumber

介绍

前言中粗略地展示了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) 的字符串程序即可得到结果。

敬请期待~~

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

时间: 2024-10-21 18:24:28

C#实现任意大数的计算和简单逻辑命题的证明——BigNumber的相关文章

ASP.NET中登录功能的简单逻辑设计

 ASP.NET中登录功能的简单逻辑设计                               概述                               逻辑设计                               1.接收参数                               2.判断参数合法性                               3.访问数据库                               4.保存Session     

卡诺图简单逻辑化简与五变量卡诺图化简

一.格雷码编码规则 画卡诺图的时候需要先将所有变量可能以格雷码的形式排列在方格两侧,所有变量有2^n个,虽然我们常用的变量为四个及以下,可以熟记格雷码,但为了学习还是有必要了解格雷码的编码规则.格雷码的基本特点就是任意两个相邻的代码只有一位二进制数不同,这样在数字电路中变化时每次就只有一位发生变化,提高了电路的稳定性. 规则: 自然二进制数到格雷码: 保留二进制码的最高位作为格雷码的最高位,而次高位格雷码为二进制码的高位与次高位相异或,而格雷码其余各位与次高位的求法相类似. 格雷码到自然二进制数

oracle初级简单逻辑备份还原

oracle初级简单逻辑备份还原 注: 1,所有命令在cmd命令提示符下运行(键入 EXP HELP=Y 或者IMP HELP=Y 获取帮助信息). 2,路径文件夹必须存在(本例中,文件夹oracleback必须存在). 单独备份用户orcl下的fruit表: exp userid=xxk/password direct=y tables=(fruit) file=d:\oracleback\baktb_fruit20140509.dmp log=d:\oracleback\baktb_frui

问题 : 来简单地数个数(大数模拟计算斐波那契数+区间数数)

题目描述 这是一个斐波那契数列: f1 = 1 f2 = 2 fn = fn-1 + fn-2    (n>=3) 蔡老板想知道,给你两个数 a.b,你能否求出在区间[a,b]里有多少个斐波那契数. 输入 多组数据输入.一行为一组输入数据,包括两个非负整数 a.b(a <= b <= 10^100),当a=b=0 时输入终止. 输出 对每组输入,输出单独一行,包含一个整数表示区间[a,b]里的斐波那契数个数. 样例输入 10 100 1234567890 9876543210 0 0 样

日志实时收集和计算的简单方案

作为互联网公司,网站监测日志当然是数据的最大来源.我们目前的规模也不大,每天的日志量大约1TB.后续90%以上的业务都是需要基于日志来完 成,之前,业务中对实时的要求并不高,最多也就是准实时(延迟半小时以上),因此,我们使用Flume将数据收集到HDFS,然后进行清洗和分析. 后来,根据业务需要,我们有了两个Hadoop集群,并且部署在不同的地方(北京和西安),而所有的日志收集服务器在北京,因此需要将日志数据通过外网传输到西安,于是有了这样的部署: 很快,通过Flume流到西安Hadoop集群的

大数常用计算模板及例题

一.模板&例题 [两个大数相加] string sum(string s1,string s2) { if(s1.length()<s2.length()) { string temp=s1; s1=s2; s2=temp; } int i,j; for(i=s1.length()-1,j=s2.length()-1;i>=0;i--,j--) { s1[i]=char(s1[i]+(j>=0?s2[j]-'0':0)); //注意细节 if(s1[i]-'0'>=10)

python的一个表达式的计算(超简单)

运行的过程如下: 输入计算表达式:3+5 计算结果:8 然后再次显示计算表达式,等待输入完成后,再次显示结果,依此循环.   作为初学者再适合不过,代码也简单,如下所示: #!/usr/bin/env python3 #coding=utf-8 import os while True: dynamic = input('输入计算表达式:') if dynamic != 'cls': try: result = eval(dynamic.lstrip().rstrip("=")) pr

ThinkCMF_X1.6.0-X2.2.3框架任意内容包含漏洞的简单分析复现(附自动化验证脚本)

1.漏洞概述 攻击者可利用此漏洞构造恶意的url,向服务器写入任意内容的文件,达到远程代码执行的目的 2.影响版本 ThinkCMF X1.6.0 ThinkCMF X2.1.0 ThinkCMF X2.2.0 ThinkCMF X2.2.1(我用的是这个) ThinkCMF X2.2.2 ThinkCMF X2.2.3 3.安装 赋予权限 4.安装系统 安装成功进入主页 Poc /?a=display&templateFile=README.md /?a=fetch&templateFi

C++大数精度计算(带小数点)

转: (原出处不可考,若有侵权,请联系我立即删除) 头文件: // WTNumber.h: interface for the CWTNumber class. // ////////////////////////////////////////////////////////////////////// #if !defined(AFX_WTNUMBER_H__92E62B40_491A_4A75_AB89_FFB160DB2343__INCLUDED_) #define AFX_WTNUM