我们可以把一个很大很长的数分成多个短小的数,然后保存在一个数组中,大数之间的四则运算及其它运算都是通过数组完成.JDK就是这么实现的.JDK的BigInteger类里用一个int数组来保存数据:

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

该int数组不会以'0'元素开头.同时该类还有一个属性来表示该数的正负.

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

1代表该数为正,0代表该数是0,-1代表该数是负数.

而本文重点分析的构造函数如下:

  1. /**
  2. * Translates the String representation of a BigInteger in the specified
  3. * radix into a BigInteger.  The String representation consists of an
  4. * optional minus sign followed by a sequence of one or more digits in the
  5. * specified radix.  The character-to-digit mapping is provided by
  6. * <tt>Character.digit</tt>.  The String may not contain any extraneous
  7. * characters (whitespace, for example).
  8. *
  9. * @param val String representation of BigInteger.
  10. * @param radix radix to be used in interpreting <tt>val</tt>.
  11. * @throws NumberFormatException <tt>val</tt> is not a valid representation
  12. *         of a BigInteger in the specified radix, or <tt>radix</tt> is
  13. *         outside the range from {@link Character#MIN_RADIX} to
  14. *         {@link Character#MAX_RADIX}, inclusive.
  15. * @see    Character#digit
  16. */
  17. 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数中,对于不同进制每次选择的位数不同,因此需要有一个数组来保存不同进制应当选择的位数,于是就有:

  1. private static int digitsPerInt[] = {0, 0, 30, 19, 15, 13, 11,
  2. 11, 10, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6,
  3. 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5};

该数组保存了java支持的最大至最小进制所对应的每次划分的位数.

该构造方法里还包含了一个相关的数组bitsPerDigit,该数组用于计算初始化mag数组的大小.

  1. // bitsPerDigit in the given radix times 1024
  2. // Rounded up to avoid underallocation.
  3. private static long bitsPerDigit[] = { 0, 0,
  4. 1024, 1624, 2048, 2378, 2648, 2875, 3072, 3247, 3402, 3543, 3672,
  5. 3790, 3899, 4001, 4096, 4186, 4271, 4350, 4426, 4498, 4567, 4633,
  6. 4696, 4756, 4814, 4870, 4923, 4975, 5025, 5074, 5120, 5166, 5210,
  7. 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如下:

  1. private static int intRadix[] = {0, 0,
  2. 0x40000000, 0x4546b3db, 0x40000000, 0x48c27395, 0x159fd800,
  3. 0x75db9c97, 0x40000000, 0x17179149, 0x3b9aca00, 0xcc6db61,
  4. 0x19a10000, 0x309f1021, 0x57f6c100, 0xa2f1b6f,  0x10000000,
  5. 0x18754571, 0x247dbc80, 0x3547667b, 0x4c4b4000, 0x6b5a6e1d,
  6. 0x6c20a40,  0x8d2d931,  0xb640000,  0xe8d4a51,  0x1269ae40,
  7. 0x17179149, 0x1cb91000, 0x23744899, 0x2b73a840, 0x34e63b41,
  8. 0x40000000, 0x4cfa3cc1, 0x5c13d840, 0x6d91b519, 0x39aa400
  9. };

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的解析附上:

  1. int superRadix = intRadix[radix];
  2. int groupVal = 0;
  3. while (cursor < val.length()) {
  4. //选取新的一串数
  5. group = val.substring(cursor, cursor += digitsPerInt[radix]);
  6. groupVal = Integer.parseInt(group, radix);//转换为十进制整数
  7. if (groupVal < 0)
  8. throw new NumberFormatException("Illegal digit");
  9. //mag*superRadix+groupVal.类似于:18927348*10^9+347389543
  10. destructiveMulAdd(mag, superRadix, groupVal);
  11. }
  12. //去掉mag数组前面的0,使得数组元素以非0开始.
  13. mag = trustedStripLeadingZeroInts(mag);
  14. private final static long LONG_MASK = 0xffffffffL;
  15. // Multiply x array times word y in place, and add word z
  16. private static void destructiveMulAdd(int[] x, int y, int z) {
  17. // Perform the multiplication word by word
  18. //将y与z转换为long类型
  19. long ylong = y & LONG_MASK;
  20. long zlong = z & LONG_MASK;
  21. int len = x.length;
  22. long product = 0;
  23. long carry = 0;
  24. //从低位到高位分别与y相乘,每次都加上之前的进位,和传统乘法一模一样.
  25. for (int i = len-1; i >= 0; i--) {
  26. //每次相乘时将x[i]转换为long,这样其32位数就可转变为其真正代表的数
  27. product = ylong * (x[i] & LONG_MASK) + carry;
  28. //x[i]取乘积的低32位.
  29. x[i] = (int)product;
  30. //高32位为进位数,留到下次循环相加
  31. carry = product >>> 32;
  32. }
  33. // Perform the addition
  34. //执行加z
  35. //mag最低位转换为long后与z相加
  36. long sum = (x[len-1] & LONG_MASK) + zlong;
  37. //mag最低位保留相加结果的低32位.
  38. x[len-1] = (int)sum;
  39. //高32位当成进位数
  40. carry = sum >>> 32;
  41. //和传统加法一样进位数不断向高位加
  42. for (int i = len-2; i >= 0; i--) {
  43. sum = (x[i] & LONG_MASK) + carry;
  44. x[i] = (int)sum;
  45. carry = sum >>> 32;
  46. }
  47. }

整个过程下来,因为保存的方法和我们脑海中那简单的存储方法会有不同,最终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方法的时候会返回原先的数串.代码如下

  1. public String toString(int radix) {
  2. if (signum == 0)
  3. return "0";
  4. if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
  5. radix = 10;
  6. // Compute upper bound on number of digit groups and allocate space
  1. //初始化字符串数组的大小,为mag数组长度一半多点.
  2. int maxNumDigitGroups = (4*mag.length + 6)/7;
  3. String digitGroup[] = new String[maxNumDigitGroups];
  4. // Translate number to string, a digit group at a time
  5. BigInteger tmp = this.abs();
  6. int numGroups = 0;
  7. while (tmp.signum != 0) {
  8. BigInteger d = longRadix[radix];
  9. MutableBigInteger q = new MutableBigInteger(),
  10. r = new MutableBigInteger(),
  11. a = new MutableBigInteger(tmp.mag),
  12. b = new MutableBigInteger(d.mag);
  1. //a除以b商保存在q中,余数保存在r中
  2. a.divide(b, q, r);
  3. BigInteger q2 = new BigInteger(q, tmp.signum * d.signum);
  4. BigInteger r2 = new BigInteger(r, tmp.signum * d.signum);
  5. //把余数转换为字符串保存在字符串数组中
  6. digitGroup[numGroups++] = Long.toString(r2.longValue(), radix);
  7. //商作为被除数
  1. <span style="WHITE-SPACE: pre"> </span>    tmp = q2;
  2. }
  3. //用StringBuilder把字符串里面的数串拼接起来,中间段的数串可能需要添加一些0
  4. // Put sign (if any) and first digit group into result buffer
  5. StringBuilder buf = new StringBuilder(numGroups*digitsPerLong[radix]+1);
  6. if (signum<0)
  7. buf.append('-');
  8. buf.append(digitGroup[numGroups-1]);
  9. // Append remaining digit groups padded with leading zeros
  10. for (int i=numGroups-2; i>=0; i--) {
  11. // Prepend (any) leading zeros for this digit group
  12. int numLeadingZeros = digitsPerLong[radix]-digitGroup[i].length();
  13. if (numLeadingZeros != 0)
  14. buf.append(zeros[numLeadingZeros]);
  15. buf.append(digitGroup[i]);
  16. }
  17. return buf.toString();
  18. }
  19. /* zero[i] is a string of i consecutive zeros. */
  20. private static String zeros[] = new String[64];
  21. static {
  22. zeros[63] =
  23. "000000000000000000000000000000000000000000000000000000000000000";
  24. for (int i=0; i<63; i++)
  25. zeros[i] = zeros[63].substring(0, i);
  26. }

上述方法核心的地方就是 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类中的方法来执行大整数除法。" 

BigInteger大数家法源代码及分析的更多相关文章

  1. Ffmpeg解析media容器过程/ ffmpeg 源代码简单分析 : av_read_frame()

    ffmpeg 源代码简单分析 : av_read_frame() http://blog.csdn.net/leixiaohua1020/article/details/12678577 ffmpeg ...

  2. C C++源代码安全分析工具调研

    C C++源代码安全分析工具调研:http://blog.csdn.net/testing_is_believing/article/details/22047107

  3. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

  4. 《Android系统源代码情景分析》连载回忆录:灵感之源

    上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView.学到了很多东西,不过也挺累的,平均不到两个 ...

  5. FFmpeg的HEVC解码器源代码简单分析:环路滤波(Loop Filter)

    ===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...

  6. FFmpeg的HEVC解码器源代码简单分析:CTU解码(CTU Decode)部分-TU

    ===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...

  7. FFmpeg的HEVC解码器源代码简单分析:CTU解码(CTU Decode)部分-PU

    ===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...

  8. FFmpeg的HEVC解码器源代码简单分析:解码器主干部分

    ===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...

  9. FFmpeg的HEVC解码器源代码简单分析:解析器(Parser)部分

    ===================================================== HEVC源代码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpe ...

随机推荐

  1. 基于jQuery右侧带缩略图导航的焦点图

    今天我们要来分享一款右侧带缩略图导航的jQuery焦点图插件,这款jQuery焦点图插件的特点是右侧有一列缩略图导航列表,并且可以定义任意数量的图片,你可以拖动列表来查看所有的图片,点击缩略图后,即可 ...

  2. 项目源码--JAVA基于LBS地理位置信息应用的服务端

    技术要点: 1. LBS应用框架服务端实现 2. JAVA服务端技术 3. MYSQL数据库技术 4. 源码带详细的中文注释 ......   详细介绍: 1. LBS应用框架服务端实现 此套源码是基 ...

  3. spring data redis使用示例

    1. 配置依赖文件 <dependencies> <dependency> <groupId>org.springframework.data</groupI ...

  4. Mybatis-Generator 自动生成Dao、Model、Mapping相关文档

    最近在学习mybatis,结果在写Mapping的映射文件时insert语句一直报错,于是想看看标准的映射文件是什么样.百度到Mybatis-Generator 自动生成Dao.Model.Mappi ...

  5. 浏览器自动化工具-Selenium

    Table of Contents 1. 什么是Selenium 2. 简单的例子 3. PS 什么是Selenium Selenium可以自动化操作浏览器,利用Selenium可以模拟用户操作,因此 ...

  6. LeetCode20 Valid Parentheses

    题意: Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the ...

  7. oracle数据库常用SQL语句

    1)删除表的一列 ALTER TABLE 表名 DROP COLUMN 列名; 2)增加表的一列 且默认值为0 alter table 表名 add 字段名 类型 default '0'; 3)修改表 ...

  8. Java SE ---控制流程:break与continue语句

    在java中,可以使用break和continue语句控制循环.     1. break语句:用于终止循环,就是跳出当前循环,执行循环后面的代码. .     2. continue语句:用于跳出当 ...

  9. 【Android 界面效果26】listview android:cacheColorHint,android:listSelector属性作用

    ListView是常用的显示控件,默认背景是和系统窗口一样的透明色,如果给ListView加上背景图片,或者背景颜色时,滚动时listView会黑掉, 原因是,滚动时,列表里面的view重绘时,用的依 ...

  10. leetcode题解: Remove Duplicates from Sorted List(已排序单链表去重)

    题目: Given a sorted linked list, delete all duplicates such that each element appear only once. For e ...