最近看了下JDK1.6版本的BigInteger类,仔细研究了下大整数实例的构造过程,现在把自己的所得所想分享给大家.
首先,为什么需要大整数类?简单的说就是因为内部的数据类型能表示的最大数是64位长度,当需要更大长度位数的数据时,基本的数据类型无法处理. 跟密码学相关的加密算法常涉及到好几百位的整数的加减乘除,因此需要设计一种有效的数据结构能够满足这样的需求.
其实要实现大整数类也不难,简单一想,我们可以把一个很大很长的数分成多个短小的数,然后保存在一个数组中,大数之间的四则运算及其它运算都是通过数组完成.JDK就是这么实现的.JDK的BigInteger类里用一个int数组来保存数据:
/**
* The magnitude of this BigInteger, in big-endian 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 (mag[0]) 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 must have
* a signum of 0. This is necessary to ensures that there is exactly one
* representation for each BigInteger value.
*
* @serial
*/
int signum;
而本文重点分析的构造函数如下:
/**
* 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
* Character.digit. 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 val.
* @throws NumberFormatException val is not a valid representation
* of a BigInteger in the specified radix, or radix is
* outside the range from {@link Character#MIN_RADIX} to
* {@link Character#MAX_RADIX}, inclusive.
* @see Character#digit
*/
public BigInteger(String val, int 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);
}
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;
}
}
初始化之后计算得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);
}
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 .