BigInteger類實例的構造過程——JDK源碼解析

        最近看了下JDK1.6版本的BigInteger類,仔細研究了下大整數實例的構造過程,現在把自己的所得所想分享給大家.    

        首先,爲什麼需要大整數類?簡單的說就是因爲內部的數據類型能表示的最大數是64位長度,當需要更大長度位數的數據時,基本的數據類型無法處理. 跟密碼學相關的加密算法常涉及到好幾百位的整數的加減乘除,因此需要設計一種有效的數據結構能夠滿足這樣的需求.

        其實要實現大整數類也不難,簡單一想,我們可以把一個很大很長的數分成多個短小的數,然後保存在一個數組中,大數之間的四則運算及其它運算都是通過數組完成.JDK就是這麼實現的.JDK的BigInteger類裏用一個int數組來保存數據:

/**
     * The magnitude of this BigInteger, in <i>big-endian</i> order: the
     * zeroth element of this array is the most-significant int of the
     * magnitude.  The magnitude must be "minimal" in that the most-significant
     * int (<tt>mag[0]</tt>) must be non-zero.  This is necessary to
     * ensure that there is exactly one representation for each BigInteger
     * value.  Note that this implies that the BigInteger zero has a
     * zero-length mag array.
     */
    int[] mag;

該int數組不會以'0'元素開頭.同時該類還有一個屬性來表示該數的正負.

/**
     * The signum of this BigInteger: -1 for negative, 0 for zero, or
     * 1 for positive.  Note that the BigInteger zero <i>must</i> have
     * a signum of 0.  This is necessary to ensures that there is exactly one
     * representation for each BigInteger value.
     *
     * @serial
     */
    int signum;

1代表該數爲正,0代表該數是0,-1代表該數是負數.

而本文重點分析的構造函數如下:

/**
     * Translates the String representation of a BigInteger in the specified
     * radix into a BigInteger.  The String representation consists of an
     * optional minus sign followed by a sequence of one or more digits in the
     * specified radix.  The character-to-digit mapping is provided by
     * <tt>Character.digit</tt>.  The String may not contain any extraneous
     * characters (whitespace, for example).
     *
     * @param val String representation of BigInteger.
     * @param radix radix to be used in interpreting <tt>val</tt>.
     * @throws NumberFormatException <tt>val</tt> is not a valid representation
     *	       of a BigInteger in the specified radix, or <tt>radix</tt> is
     *	       outside the range from {@link Character#MIN_RADIX} to
     *	       {@link Character#MAX_RADIX}, inclusive.
     * @see    Character#digit
     */
    public BigInteger(String val, int radix) {

        該構造函數就是把一個字符串val所代表的的大整數轉換並保存mag數組中,並且val所代表的字符串可以是不同的進制(radix決定).

        分析該構造函數源碼之前,先想一個問題,構造一個大整數開始最主要的問題是如何把一個大數保存到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數組的大小.

 // bitsPerDigit in the given radix times 1024
    // Rounded up to avoid underallocation.
    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] = 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 = new int[numWords];
        //開始按照digitsPerInt截取字符串裏的數
        //將不夠digitsPerInt[radix]的先取出來轉換
        int firstGroupLen = numDigits % digitsPerInt[radix];
    	if (firstGroupLen == 0)
    	    firstGroupLen = digitsPerInt[radix];
    	//把第一段的數字放入mag數組的最後一位
    	String group = val.substring(cursor, cursor += firstGroupLen);
            mag[mag.length - 1] = Integer.parseInt(group, radix);
    	if (mag[mag.length - 1] < 0)
    	    throw new NumberFormatException("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)
			throw new NumberFormatException("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] (18927348*10^9+347389543)
        第二次循環後: [1026053]   [-1675546271]   [440884830]. ((18927348*10^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
        //初始化字符串數組的大小,爲mag數組長度一半多點.
	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(),
                              r = new MutableBigInteger(),
                              a = new MutableBigInteger(tmp.mag),
                              b = new MutableBigInteger(d.mag);
         //a除以b商保存在q中,餘數保存在r中
            a.divide(b, q, r);
            BigInteger q2 = new BigInteger(q, tmp.signum * d.signum);
            BigInteger r2 = new BigInteger(r, tmp.signum * d.signum);
         //把餘數轉換爲字符串保存在字符串數組中
            digitGroup[numGroups++] = Long.toString(r2.longValue(), radix);
         //商作爲被除數
	    tmp = q2;
	}
     //用StringBuilder把字符串裏面的數串拼接起來,中間段的數串可能需要添加一些0
	// 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 zeros
	for (int i=numGroups-2; i>=0; i--) {
	    // Prepend (any) leading zeros for this digit group
	    int numLeadingZeros = digitsPerLong[radix]-digitGroup[i].length();
	    if (numLeadingZeros != 0)
		buf.append(zeros[numLeadingZeros]);
	    buf.append(digitGroup[i]);
	}
	return buf.toString();
    }

    /* zero[i] is a string of i consecutive zeros. */
    private static String zeros[] = new String[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多行的代碼....若有興趣的話仔細看看其中某個方法對我們可能會有啓發.

       大家有不同意的地方希望能指出,多多交流,還有最好能幫我把遺留的divide方法給解釋一下,我看了好久快看噁心了,大神在哪裏!!!!

       本文參考的文章鏈接:http://www.iteye.com/topic/1118707 .


發佈了42 篇原創文章 · 獲贊 32 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章