介紹
本篇是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) 的字符串程序即可得到結果。
敬請期待~~
轉載本博客上的原創文章者,請注明出處