转载自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=1000000000intRadix[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多行的代码….若有兴趣的话仔细看看其中某个方法对我们可能会有启发.

java BigInteger源码学习的更多相关文章

  1. java Integer 源码学习

    转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的 ...

  2. Java集合源码学习(一)集合框架概览

    >>集合框架 Java集合框架包含了大部分Java开发中用到的数据结构,主要包括List列表.Set集合.Map映射.迭代器(Iterator.Enumeration).工具类(Array ...

  3. Java集合源码学习(三)LinkedList分析

    前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ...

  4. Java集合源码学习(三)LinkedList

    前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ...

  5. Java集合源码学习(五)几种常用集合类的比较

    这篇笔记对几个常用的集合实现,从效率,线程安全和应用场景进行综合比较. >>ArrayList.LinkedList与Vector的对比 (1)相同和不同都实现了List接口,使用类似.V ...

  6. Java集合源码学习(四)HashMap分析

    ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据 ...

  7. Java集合源码学习(二)ArrayList分析

    >>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ...

  8. Java集合源码学习(四)HashMap

    一.数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景,数组的特点是:寻址容易,插入和删除困难:链表的特点是:寻址困难,插入和删除容易:哈希表的实现结合了这两点, ...

  9. Java集合源码学习(二)ArrayList

    1.关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫"Arr ...

随机推荐

  1. Appium Android Bootstrap源码分析之简介

    在上一个系列中我们分析了UiAutomator的核心源码,对UiAutomator是怎么运行的原理有了根本的了解.今天我们会开始另外一个在安卓平台上基于UiAutomator的新起之秀--Appium ...

  2. SQL点滴29—错误无处不在

    原文:SQL点滴29-错误无处不在 我只想说以下是很基础的sql知识,但是很容易犯错.所以睁大我们的眼睛,屏住我们的呼吸,小心的检查吧! 案例1if not exists (select OrderI ...

  3. System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(string, string)已过时的解决办法

    FormsAuthentication.HashPasswordForStoringInConfigFile 方法是一个在.NET 4.5中已经废弃不用的API,参见: https://msdn.mi ...

  4. SQL Server 性能调优 之运行计划(Execution Plan)调优

    运行计划中的三种 Join 策略 SQL Server 存在三种 Join 策略:Hash Join,Merge Join,Nested Loop Join. Hash Join:用来处理没有排过序/ ...

  5. QtNetwork说明(两)使用QT实现360的ctrl+ctrl特征

    头文字说明: <span style="font-size:18px;">#ifndef GOOGLESUGGEST_H #define GOOGLESUGGEST_H ...

  6. 终于有人把O2O、C2C、B2B、B2C的区别讲透了!

    终于有人把O2O.C2C.B2B.B2C的区别讲透了! 一.O2O.C2C.B2B.B2C的区别在哪里? O2O是online to offline分为四种运营模式: 1.online to offl ...

  7. MySql处理数据库和表

    show databases; mysql> show databases; mysql> create database db_test; Query OK, 1 row affecte ...

  8. NUint使用详解及Visual Studio配置

    NUint使用详解及Visual Studio配置 阅读目录 什么是单元测试? 为什么使用单元测试? NUint使用详解: 示例 属性 断言 简单测试 VS配置: External Tools Vis ...

  9. Effective C++(19) 设计class犹如设计type

    问题聚焦:     这一节不涉及代码,但是我们需要明确的一点是,思想比代码要重要得多.     设计优秀的classes是一项艰巨的工作,就像设计好的types一样.     我们应该带着和“语言设计 ...

  10. CSDN 高校俱乐部: 排列搜索

    CSDN 高校俱乐部/英雄会 题目: 设数组a包含n个元素恰好是0..n - 1的一个排列,给定b[0],b[1],b[2],b[3]问有多少个0..n-1的排列a,满足(a[a[b[0]]]*b[0 ...