轉載自http://www.hollischuang.com/archives/176
在java中,有很多基本數據類型我們可以直接使用,比如用於表示浮點型的float、double,用於表示字符型的char,用於表示整型的int、short、long等。但是,拿整數來說,如果我們想要表示一個非常大的整數,比如說超過64位,那么能表示數字最大的long也無法存取這樣的數字時,我們怎么辦。以前的做法是把數字存在字符串中,大數之間的四則運算及其它運算都是通過數組完成。JDK也有類似的實現,那就是
BigInteger
。
什么是BigInteger(定義)
BigInteger類的基本結構如下所示:
java.lang.Object |_java.lang.Number |_java.math.BigInteger
BigInteger已實現的接口:Serializable, Comparable
類定義如下:
public class BigInteger extends Number implements Comparable<BigInteger>{}
BigInteger
是不可變的任意精度的整數。所有操作中,都以二進制補碼形式表示 BigInteger
(如 Java 的基本整數類型)。BigInteger
提供所有 Java 的基本整數操作符的對應物,並提供 java.lang.Math
的所有相關方法。另外,BigInteger
還提供以下運算:模算術、GCD 計算、質數測試、素數生成、位操作以及一些其他操作。
屬性
下面看看BigInteger有哪些重點的屬性,主要的有下面兩個:
final int signum
signum屬性是為了區分:正負數和0的標志位,整數用1表示,負數用-1表示,零用0表示。
final int[] mag
mag是magnitude的縮寫形式,mag數組是存儲BigInteger數值大小的,采用big-endian的順序,也就是高位字節存入低地址,低位字節存入高地址,依次排列的方式。
我們來分析一下為什么BigInteger中要有這兩個成員變量。 我們知道,BigInteger存儲大數的方式就是將數字存儲在一個整型的數組中(具體怎么存,后面有談),這樣就能解決可以存很多很多位數字的問題。那么,只用一個整型數組的話,如何表示一個整數的正負呢?那么就需要有一個單獨的成員變量來標明該數的正負。
構造函數
public BigInteger(byte[] val) { if (val.length == 0) throw new NumberFormatException("Zero length BigInteger"); if (val[0] < 0) { mag = makePositive(val); //這個函數的作用是將負數的byte字節數組轉換為正值。 signum = -1; //如果數組第一個值為負數,則將數組變正存入mag,signum賦-1 } else { mag = stripLeadingZeroBytes(val);//如果非負,則可直接去掉前面無效零,再賦給mag signum = (mag.length == 0 ? 0 : 1); } }
將包含 BigInteger
的二進制補碼表示形式的 byte 數組轉換為 BigInteger
。輸入數組假定為 big-endian
字節順序:最高有效字節在第零個元素中。
再來看另外一種構造BigInteger
的方式:public BigInteger(String val)
這個構造函數接收一個字符串,然后直接將字符串轉換成BigInteger
類型。
public static void main(String[] args) { BigInteger bigInteger = new BigInteger("123456789987654321123456789987654321123456789987654321"); System.out.println(bigInteger); }
這看起來很方便,只要我們明確的知道我們想要的數字的字符串形式,就可以直接用他構造一個BigInteger
接着,我們就分析一下這個函數是怎么實現的,難道只是把我們傳入的字符串直接存到mag
數組里面了么?以下是該構造函數的實現:
public BigInteger(String val) { this(val, 10); }
這個函數調用了另外一個構造方法,那么我們就直接分析這個構造方法: public BigInteger(String val, int radix)
該構造函數就是把一個字符串val所代表的的大整數轉換並保存mag數組中,並且val所代表的字符串可以是不同的進制(radix決定),比如,我們這樣構造一個BigInteger:BigInteger bigInteger = new BigInteger("101",2);
,那么我們得到的結果就是5。
分析該構造函數源碼之前,先想一個問題,構造一個大整數開始最主要的問題是如何把一個大數保存到mag數組中,通常我們自己實現的話很有可能是數組每塊存一位數(假設大數為10進制),但這樣的話想想也知道太浪費空間,因為一個int值可以保存遠不止一位十進制數. Java語言里每個int值大小范圍是-2^31至2^31-1
即-2147483648~2147483647
,因此一個int值最多可保存一個10位十進制的整數,但是為了防止超出范圍(2222222222這樣的數int已經無法存儲),保險的方式就是每個int保存9位的十進制整數.JDK里的mag數組即是這樣的保存方式. 因此若一串數為:18927348347389543834934878. 划分之后就為:18927348 | 347389543 | 834934878. mag[0]
保存18927348 ,mag[1]
保存347389543 ,mag[2]
保存834934878 這樣划分可以最大利用每一個int值,使得mag數組占用更小的空間.當然這只是第一步.
划分的問題還沒有說完,上述構造函數能夠支持不同進制的數,最終轉換到mag數組里面的數都是十進制,那么不同進制的大數,每次選擇划分的位數就不相同,若是2進制,每次就可以選擇30位來存儲到一個int數中(int值大小范圍是-2^31至2^31-1),若是3進制3^19<2147483647<3^20,因此每次就可以選擇19位來存儲到一個int數中,對於不同進制每次選擇的位數不同,因此需要有一個數組來保存不同進制應當選擇的位數,於是就有:
private static int digitsPerInt[] = {0, 0, 30, 19, 15, 13, 11, 11, 10, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5};
該數組保存了java支持的最大至最小進制所對應的每次划分的位數
該構造方法里還包含了一個相關的數組bitsPerDigit,該數組用於計算初始化mag數組的大小.
private static long bitsPerDigit[] = { 0, 0, 1024, 1624, 2048, 2378, 2648, 2875, 3072, 3247, 3402, 3543, 3672, 3790, 3899, 4001, 4096, 4186, 4271, 4350, 4426, 4498, 4567, 4633, 4696, 4756, 4814, 4870, 4923, 4975, 5025, 5074, 5120, 5166, 5210, 5253, 5295};
“bitsPerDigit是用於計算radix進制m個有效數字 轉換成2進制所需bit位[假設所需x位],我們來看一個計算式:radix^m – 1 = 2^x – 1, 解這個方程得 x = m * log2(radix) , 現在m是幾位有效數字,常量就只有 log2(radix),這是一個小數,這不是我們喜歡的,所以我們希望用一個整數來表示,於是我們把他擴大1024倍然后取整,例如3進制
bitsPerDigit[3][3] = 1624
(我用計算器算了一下 x = log2(3) * 1024 ~= 1623.xxx) ,我們隊這個數取整,為什么取1624呢,其實只要不超過太多都可以的,你可以設置為1620,1600,1610…;”
也就是說對於一串數(N進制),其轉換成二進制的位數再乘以1024就是bitsPerDigit數組里面對應的數據,乘以1024再取整可能讓人看着舒服吧.
有了以上的介紹之后,我們現在可以貼上該方法的源代碼仔細看看.
public BigInteger(String val, int radix) { int cursor = 0, numDigits; int len = val.length();//獲取字符串的長度 //不符合條件的情況 if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) throw new NumberFormatException("Radix out of range"); if (val.length() == 0) throw new NumberFormatException("Zero length BigInteger"); //判斷正負,處理掉字符串里面的"-" signum = 1; int index = val.lastIndexOf("-"); if (index != -1) { if (index == 0) { if (val.length() == 1) throw new NumberFormatException("Zero length BigInteger"); signum = -1; cursor = 1; } else { throw new NumberFormatException("Illegal embedded minus sign"); } } //跳過前面的0 while (cursor < len && Character.digit(val.charAt(cursor),radix) == 0) cursor++; if (cursor == len) {//若字符串里全是0,則存儲為ZERO.mag signum = 0; mag = ZERO.mag; return; } else {//numDigits為實際的有效數字 numDigits = len - cursor; } //numDigits位的radix進制數轉換為2進制需要多少位 //bitsPerDigit數組里面的元素乘了1024這里就需要右移10位(相當於除以1024),做除法的時候會有 //小數的丟失,因此加1確保位數一定夠 //一個int有32bit,因此除以32即是我們開始估算的mag數組的大小 int numBits =(int)(((numDigits * bitsPerDigit[radix])>>>10)+1);int numWords =(numBits +31)/32; mag =newint[numWords];//開始按照digitsPerInt截取字符串里的數 //將不夠digitsPerInt[radix]的先取出來轉換 int firstGroupLen = numDigits % digitsPerInt[radix];if(firstGroupLen ==0) firstGroupLen = digitsPerInt[radix];//把第一段的數字放入mag數組的最后一位 Stringgroup= val.substring(cursor, cursor += firstGroupLen); mag[mag.length -1]=Integer.parseInt(group, radix);if(mag[mag.length -1]<0)thrownewNumberFormatException("Illegal digit");//剩下的一段段轉換 int superRadix = intRadix[radix];int groupVal =0;while(cursor < val.length()){group= val.substring(cursor, cursor += digitsPerInt[radix]); groupVal =Integer.parseInt(group, radix);if(groupVal <0)thrownewNumberFormatException("Illegal digit"); destructiveMulAdd(mag, superRadix, groupVal);} mag = trustedStripLeadingZeroInts(mag);}
現在我對最后的幾行還沒有分析,是因為有一個intRadix
數組我們還沒有解釋.intRadix
數組其實就是一個保存了對應各種radix的最佳進制的表, 上面我們說過了對於十進制我們選擇一次性截取9位數,這樣能充分利用一個int變量同時還可保證不超出int的范圍,因此intRadix[10]=10^9=1000000000
. intRadix[3]=3^19=1162261467
. 也就是每次截取的數都不會超過其radix
對應的最佳進制.舉例 十進制數18927348347389543834934878
其最終轉換為:
18927348*(10^9)^2 +347389543*(10^9)+834934878,最終從整體上來看mag數組保存的是一個10^9進制的數.
intRadix如下:
private static int intRadix[] = {0, 0, 0x40000000, 0x4546b3db, 0x40000000, 0x48c27395, 0x159fd800, 0x75db9c97, 0x40000000, 0x17179149, 0x3b9aca00, 0xcc6db61, 0x19a10000, 0x309f1021, 0x57f6c100, 0xa2f1b6f, 0x10000000, 0x18754571, 0x247dbc80, 0x3547667b, 0x4c4b4000, 0x6b5a6e1d, 0x6c20a40, 0x8d2d931, 0xb640000, 0xe8d4a51, 0x1269ae40, 0x17179149, 0x1cb91000, 0x23744899, 0x2b73a840, 0x34e63b41, 0x40000000, 0x4cfa3cc1, 0x5c13d840, 0x6d91b519, 0x39aa400 };
intRadix[10]=0x3b9aca00 = 1000000000; intRadix[3]=0x4546b3db=1162261467;
我們注意到 numWords = (numBits + 31) /32
. 初始數組的大小並不是大整數划分的數目而是將計算大整數對應的二進制位數(加上31確保numWords大於0)然后除以32得到,因此mag數組中每一個int數的32位是被完全利用的,也就是把每個int數當成無符號數來看待.若不完全利用int的32位的話,我們完全可以根據划分的結果來確定mag數組的初始大小,之前的例子:18927348 | 347389543 | 834934878,我們知道10進制數每次選擇9位不會越界,我們可以直觀的得到mag數組的大小為3,但是這樣的話每個int元素仍然有些空閑的位沒有利用.
因此我們之前的划分方法只是整個數組初始化的想象中第一步. 這個例子按照numWords = (numBits + 31) /32這樣計算最后得到的應當仍是3.但是若是再大一些的數串結果就不一定一樣,積少成多,很大的數串時節省的空間就能體現出來啦.
Java沒有無符號int數,因此mag數組中常常會符號為負的元素. 而最終把原大整數轉換為mag數組保存的radix對應的最佳進制數的過程由destructiveMulAdd
完成.現在把構造函數的最后一部分的和方法destructiveMulAdd
的解析附上:
int superRadix = intRadix[radix]; int groupVal = 0; while (cursor < val.length()) { //選取新的一串數 group = val.substring(cursor, cursor += digitsPerInt[radix]); groupVal = Integer.parseInt(group, radix);//轉換為十進制整數 if (groupVal < 0) throw new NumberFormatException("Illegal digit"); //mag*superRadix+groupVal.類似於:18927348*10^9+347389543 destructiveMulAdd(mag, superRadix, groupVal); } //去掉mag數組前面的0,使得數組元素以非0開始. mag = trustedStripLeadingZeroInts(mag); private final static long LONG_MASK = 0xffffffffL; // Multiply x array times word y in place, and add word z private static void destructiveMulAdd(int[] x, int y, int z) { // Perform the multiplication word by word //將y與z轉換為long類型 long ylong = y & LONG_MASK; long zlong = z & LONG_MASK; int len = x.length; long product = 0; long carry = 0; //從低位到高位分別與y相乘,每次都加上之前的進位,和傳統乘法一模一樣. for (int i = len-1; i >= 0; i--) { //每次相乘時將x[i]轉換為long,這樣其32位數就可轉變為其真正代表的數 product = ylong * (x[i] & LONG_MASK) + carry; //x[i]取乘積的低32位. x[i] = (int)product; //高32位為進位數,留到下次循環相加 carry = product >>> 32; } // Perform the addition //執行加z //mag最低位轉換為long后與z相加 long sum = (x[len-1]& LONG_MASK)+ zlong;//mag最低位保留相加結果的低32位. x[len-1]=(int)sum;//高32位當成進位數 carry = sum >>>32;//和傳統加法一樣進位數不斷向高位加 for(int i = len-2; i >=0; i--){ sum =(x[i]& LONG_MASK)+ carry; x[i]=(int)sum; carry = sum >>>32;}}
整個過程下來,因為保存的方法和我們腦海中那簡單的存儲方法會有不同,最終mag數組里的元素跟原先的字符串就會有很大的不同,但實質上還是表示着相同的數,現把18927348347389543834934878例子的構造過程展示出:
初始化之后計算得numBits=87,這樣數組初始化大小numWords=3. 進入最終的循環前mag數組:[0] [0] [18927348] 第一次循環后: [0] [4406866] [-1295432089] (1892734810^9+347389543) 第二次循環后: [1026053] [-1675546271] [440884830]. ((1892734810^9+347389543)*10^9+834934878) 最終我們就把18927348347389543834934878 轉換成10^9進制的數保存到了mag數組中.雖然最終的結果我們讓我們不太熟悉,但是其中數串划分的方法和數組節省空間的思想都是值得學習的
現在有最后一個問題,如何mag數組轉換為原來的數串呢?JDK里面是通過不斷做除法取余實現的,BigInteger類的實例在調用toString方法的時候會返回原先的數串.代碼如下:
public String toString(int radix) { if (signum == 0) return "0"; if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; // Compute upper bound on number of digit groups and allocate space int maxNumDigitGroups = (4*mag.length + 6)/7; String digitGroup[] = new String[maxNumDigitGroups]; // Translate number to string, a digit group at a time BigInteger tmp = this.abs(); int numGroups = 0; while (tmp.signum != 0) { BigInteger d = longRadix[radix]; MutableBigInteger q = new MutableBigInteger(), a = new MutableBigInteger(tmp.mag), b = new MutableBigInteger(d.mag); MutableBigInteger r = a.divide(b, q); BigInteger q2 = q.toBigInteger(tmp.signum * d.signum); BigInteger r2 = r.toBigInteger(tmp.signum * d.signum); digitGroup[numGroups++] = Long.toString(r2.longValue(), radix); tmp = q2; } // Put sign (if any) and first digit group into result buffer StringBuilder buf = new StringBuilder(numGroups*digitsPerLong[radix]+1); if (signum<0) buf.append('-'); buf.append(digitGroup[numGroups-1]);// Append remaining digit groups padded with leading zerosfor(int i=numGroups-2; i>=0; i--){// Prepend (any) leading zeros for this digit groupint numLeadingZeros = digitsPerLong[radix]-digitGroup[i].length();if(numLeadingZeros !=0) buf.append(zeros[numLeadingZeros]); buf.append(digitGroup[i]);}return buf.toString();}privatestaticString zeros[]=newString[64];static{ zeros[63]="000000000000000000000000000000000000000000000000000000000000000";for(int i=0; i<63; i++) zeros[i]= zeros[63].substring(0, i);}
上述方法核心的地方就是 a.divide(b, q, r). longRadix數組和intRadix數組有着相似的涵義.
intRadix[10]=10^9.因此longRadix[10]=10^18,相當於對intRadix進行了平方,也就是對long類型來說的最佳進制數.
簡單的想一下可以明白:mag數組若是不斷除以10^9可以得到834934878,347389543,18927348最終可獲得原先字符串.若是除以10^18(Java支持該數量級的運算),兩次分別得到:34738954318927348,834934878,因此使用longRadix數組運算的效率更高. 對於上述方法出現的類MutableBigInteger,借用網上的一段話解釋可能比我說的更好些:
“MutableBigInteger是BigInteger類的另一個版本,它的特點是不創建臨時對象的前提上使調用程序得到象BigInteger類型的返回值(稱為可變對象技術)。因為大整數的除法是由大量的其他算術操作組成的,所以需要大量的臨時對象,而完成大量的操作而不創建新的對象可以極大地改善程序的性能,(因為創建對象的代價是很高的)所以在Java的大整數類中使用MutableBigInteger類中的方法來執行大整數除法。”
而最為關鍵的divide方法不好意思啊我看了好久仍然是沒有弄懂代碼的思路,希望大家能夠指點迷津!
JDK的BigInteger類中還實現了好多方法都值得我們一看,除了基本的四則元素外,里面還提供了判斷素數的方法,求冪,求模,求逆元,求最大公約數,用到了Miller-Rabin算法,滑動窗口算法快速求冪(我看了看好像是),歐幾里得算法,中國剩余定理等,3000多行的代碼….若有興趣的話仔細看看其中某個方法對我們可能會有啟發.