noteless 头像

[十六]基础类型BigInteger简介

 
 
BigInteger和BigDecimal都是Java针对大数提供的类
超出了java的表示范围
image_5bd81e9b_5f09
 

属性简介

借助于signummag 来实现数据的符号位和实际数据的保存
final int signum 保存BigInteger的符号
负数 -1
0 0
正数 1
 
final int[] mag;保存数字的数据
字节序为大端模式,大端模式就是低地址存储高位
 
数组的第一个元素必须是非0的,也就是如果有前导零将会被移除
这样可以保证每个数都有一个唯一的表示形式
这种要求下 BigInteger的0有一个0长度的数组保存
对于BigInteger 他的数据打开就是这么一种形式
 
[ 101....32位....1] [ 110....32个....1] ....N个..... [ 0110....32个....1]
 
它的真值的计算方法与其他的二进制序列一样的
二进制为 0111 1110    的十进制为126 相信谁都会计算,BigInteger也是如此的
 
尤其是对于BigInteger字符串参数的构造形式
千万不要以为就是把字符的编码或者字符转换成数字切段存放到int数组中
他存放的都是转换后的真值
下面会详细介绍
 

使用字节数组构造

内部是Int数组,一个int 32位就是 4个字节,所以自然是可以使用字节对BigInteger进行构造的
提供了两种形式的字节构造方法,可以指定符号的
image_5bd81e9b_4bfe
使用字节进行构造,就是把所有的字节填充到int数组中
不过要注意的是,
计算机中存储的数值都是补码的形式
正数的补码与原码相同
负数的补码是他的原码取反再加一
就是把这些字节的补码按照顺序拼在一起,组合成int数组
  • 如果是一个负数,会先得到真值的绝对值
  • 如果有前导零,还会去掉所有的前导零
而且,是大端排序,大端排序,大端排序的把最终的数据存储起来
也就是说int数组中保存的都是真值的绝对值,使用专门的符号位记录正负和0
 

原码/反码/补码

先回顾下原码/反码/补码 的概念
原码

符号位+数值位
符号位为0 表示正数,符号位为1 表示负数
 
数值位就是真值的绝对值
又被称为带符号的绝对值表示
反码 正数的反码为其原码
负数的反码为其原码的除符号位外,逐位取反
补码 正数的补码为其原码
负数的补码为其反码+1 
 

补码计算步骤

第一步求原码: 先写出来她的原码--->符号位+数值位(绝对值)
第二步求反码:
如果是正数 反码与原码一样
如果是负数 反码为原码取反(除符号位外,逐位翻转) 
第三步求补码:
如果是正数 补码与原码一样
如果是负数 补码为反码 + 1
第四步扩充:
如果不足数据类型的宽度,将需要填充到指定宽度
符号位扩充,也就是正数补0  负数补1
总结
不管什么形式,第一位始终都是符号位,0 表示正数, 1表示负数
正数原码/反码/补码 全都一样,知道一种就直接得到另外的形式
负数如果知道补码,想要得到他的原码,只需要对补码再一次的求补码即可
 

示例1

image_5bd81e9b_66a8
 

示例2

image_5bd81e9b_24ee
 
通过这两个例子应该可以看得出来,数值都是补码形式存放
字节存储的也是补码 , int存储的也是补码,
所以使用字节构造 就是把所有的补码拼凑在一起就好了 
拼凑排列好的补码,如果是正数,那么原码/反码/补码全都一样,存储的就是这个值
如果是负数,还需要取他的绝对值,绝对值就是 再求一次补码,去掉符号位就是绝对值了
BigInteger数组中,存储的都是真值的绝对值的补码,真值绝对值得补码,其实就是原码去掉符号位嘛,一个意思
就像上面的第二个例子  得到的补码为:  
1000  0011  1111 0111  0000  0000  0101 1001
实际存储的是:
0111  1100   0000 1000  1111  1111 1010  0111
 

 

使用String构造

String作为参数的构造方法有两种形式
本质上只是一种,那就是指定基数的字符串转换为BigInteger
简化形式就是默认十进制的形式
image_5bd81e9c_6476
 
通过String构造BigInteger的逻辑比较简单,但是实现看起来会有些令人模糊
接下来我们先介绍一点相关的计算基础

算法基础

int能够支撑的数据长度以及基数
我们知道,存储实际数据的是int数组
int表示范围是:   
-231 ~ 231-1     也就是   -2147483648 ~ 2147483647
对于十进制
可以表示10位十进制数字
但是 2147483648 (2147483647+1)  仍旧是10位数字却溢出了
所以选择保存9位十进制数
 
所以每个int   十进制下最大值为10的9次方
image_5bd81e9c_6544
对于二进制
最大值 231-1 ,所以只能保存30位 2进制数
 
所以每个int   二进制下最大值为2的30次方
image_5bd81e9c_595a
对于三进制
319 =1162261467 <2147483647<320 = 3486784401  
所以能够保存19位 三进制数
 
所以每个int   三进制下最大值为3的19次方
image_5bd81e9c_3b88
对于四进制
415 = 1073741824 < 2147483647 < 416 = 4294967296  
所以能够保存15位 四进制数
 
所以每个int  四进制下最大值为4的15次方
image_5bd81e9c_33dd
对于十六进制
167 =268435456 < < 2147483647 < 168 =  4294967296
所以能够保存7位十六进制数

所以每个int  十六进制下最大值为16的7次方
image_5bd81e9c_1fb5
所以就有了这么两个映射数组
digitsPerInt
表示每个int 可以表示的,指定进制下数字的位数,下标索引就是进制基数
比如可以表示十六进制的位数为digitsPerInt[16] = 7
intRadix
表示每个int可以表示的指定进制下的最大值,下标索引就是进制基数
比如 每一位int  可以表示的十进制的最大值为  intRadix[10] = 0x3b9aca00=1,000,000,000
image_5bd81e9c_957
其实intRadix这个数就是:
BigInteger在这个基数下的基数
这句话有点绕,BigInteger内部是数组,假如为mag[0] mag[1]    intRadix[10] = 0x3b9aca00
那么也就是,BigInteger在十进制,也就是10为基数下的基数为0x3b9aca00
那么这个值就是 mag[0] x 0x3b9aca001   + mag[1] x 0x3b9aca00
0  
就如同十进制的数12的计算方式为1x101 + 2 x100 =12 一样的道理
下面还会继续说明
同理它内部也有两个针对Long 的数组,用于内部计算使用
 
BigInteger内部使用int数组表示
普通数值使用每个数值位上的数字进行表示
一个BigInteger有多个int
一个普通数值有多个数字位

每个int能够表示的指定进制的最大值--intRadix 中保存的数据
其实 就是 BigInteger 的基于每个int作为一个元素的进制基数
 
image_5bd81e9c_1e0
 
假设R为指定的基数
L为指定基数的数字的长度

那么用多少位2进制数可以表示?

x位二进制能够表示的最大值为
image_5bd81e9c_125
L位R进制的数能够表示的最大值为
image_5bd81e9c_6d2b
比如R=10 L=2 也就是十进制两位数能够表示的最大值为: 10的平方减1     等于 99
image_5bd81e9d_3e48
解上面的方程,可以得出来
x的长度为 :L    乘以     以2为底R的对数
内部还有一个数组
这个数组的值就是以2为底R的对数的值,然后乘以1024,然后进行向上取整
image_5bd81e9d_534e
bitsPerDigit 就是每个数字需要的比特位数乘以1024后在取整
之所以乘以1024然后在取整
应该是为了简化运算,这个数必然要是2的N次方,计算机移位最快
当然,这个地方乘以1024 实际使用的时候必然也还得除以1024
image_5bd81e9d_6359
以2为底 2的对数 =  1                        * 1024 = 1024
以2为底 3的对数 = 1.5849625007      * 1024 = 1623.0016007168 -> 1624
以2为底 4的对数 =  2                        * 1024 = 2048
以2为底 5的对数 =   2.3219280949     * 1024 = 2377.6543691776 ->2378
以2为底 10的对数 =  3.3219280949   * 1024=3401.6543691776 -> 3402
以2为底 16的对数 =  4                      * 1024 = 4096
 
说到这,我们再回头看看上面介绍的几个数组
 
digitsPerInt  表示不同基数(进制)下一个int 能够表示的数字的长度 ,这个位数其实就是按照多长进行分割组装
intRadix  就是基数
bitsPerDigit  是用来推算需要多少个int的,也就是int数组的长度
 
以上是String构造BigInteger的用到的一些基本概念
 
我们以一个最简单的例子进行演示:
计算字符串 "123"  十进制表示的数值
使用数组mag 来进行存储每一位数字
显然需要mag[3] 不要纠结mag类型,此处只是为了示例
1. 找到第一个字符  "1" ,转换为数字1, 然后保存到mag[3] = 1 (我们此处假定从数组最后开始存放)
2. 找到第二个字符  "2" , 转换为数字2,然后 计算 mag[3] x 10 +2  
mag[3] x 10 = 10 ,结果进行保存
mag[2] 保存1   mag[3] 保存0  
然后再加上2   0+2 = 2 不用进位
所以最终结果为mag[3] = 2  mag[2] = 1
3. 找到第三个字符  "3" , 转换为数字3,然后 计算 (mag[2]mag[3]) x 10 +3
mag[2]mag[3]  就相当于是两位数 比如12
image_5bd81e9d_4d57
此时 mag[3] = 0  mag[2] = 2   mag[0] = 1 
然后还需要加 3
mag[3] + 3 = 0+3 = 3 也没有进位
那么最终结果为
mag[0] = 1  mag[2] = 2  mag[3] = 3
以上就是一个简单的从字符串123 转换为10进制数,并且保存到数据的过程
String的构造就是类似这样的一个过程
 
 

构造方法源码解析

我们从构造方法入手,简单介绍下内部是如何运作的

public BigInteger(String val, int radix) {

//定义了两个变量一个光标,光标记录着应该要处理的数据索引下标

//另一个numDigits 用来保存需要处理的数字位数 也就是有效长度,比如去掉前导零后的

int cursor = 0, numDigits;

final int len = val.length();//传递进来的字符数组的长度

 

//如果给定的基数,不在合法范围内,那么抛出异常,不会默认处理

if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)

throw new NumberFormatException("Radix out of range");

//如果字符串长度为0 也是一种非法的参数

if (len == 0)

throw new NumberFormatException("Zero length BigInteger");

// Check for at most one leading sign

int sign = 1;

int index1 = val.lastIndexOf('-');

int index2 = val.lastIndexOf('+');

//符号- + 只能出现一个,而且还必须是第一个位置,否则都不合法

//根据最后一个的索引与0 进行比较,可以简便的判断符号位是否合法

if (index1 >= 0) {

if (index1 != 0 || index2 >= 0) {

throw new NumberFormatException("Illegal embedded sign character");

}

sign = -1;

cursor = 1;

} else if (index2 >= 0) {

if (index2 != 0) {

throw new NumberFormatException("Illegal embedded sign character");

}

cursor = 1;

}

//经过前面的判断,如果有符号位的话,光标的值更新为1 也就是后续不处理符号位

//如果此时光标的值等于字符长度,说明没有有效数字了,将会抛出异常

if (cursor == len)

throw new NumberFormatException("Zero length BigInteger");

 

// Skip leading zeros and compute number of digits in magnitude

//如果有前导0 ,将会去掉这些,光标的位置也会跟着一起移动

while (cursor < len &&

Character.digit(val.charAt(cursor), radix) == 0) {

cursor++;

}

 

//跳过了所有的0之后就不再有有效数据了,说明他就是个0

//哪怕他原来设置的负数的0 将会变为0 的标记

if (cursor == len) {

signum = 0;

mag = ZERO.mag;

return;

}

 

//记录实际需要处理的数据长度以及对符号位使用signum进行记录

numDigits = len - cursor;

signum = sign;

 

// Pre-allocate array of expected size. May be too large but can

// never be too small. Typically exact.

//根据前面的公式计算实际需要的二进制位数 numDigits需要处理的数字的长度

//bitsPerDigit 里面记录了每个进制1位数需要的二进制位数,但是放大了1024倍,所以还要除以1024 也就是右移10

//真正的值可能是小数个,除以1024之后变成了取整了,然后再加上一,百分百够用,需要的比特位数保存到numBits

long numBits = ((numDigits * bitsPerDigit[radix]) >>> 10) + 1;

if (numBits + 31 >= (1L << 32)) {

reportOverflow();

}

//numWords 记录的是实际需要的int类型数据的个数,也就是数组的长度

//右移5位就是除以32 就是计算数组的长度,除法会取整,防止1个不足32位的时候,就会变成0了所以numBits加上31 之后再除以32

int numWords = (int) (numBits + 31) >>> 5;

//此时创建真正的保存数据的int数组了

int[] magnitude = new int[numWords];

 

// Process first (potentially short) digit group

//numDigits 需要处理的数字的个数

//digitsPerInt 保存的是每一个int能够保存的指定数制下的字符长度

//如果有余数,说明有一个不足最大长度的位数

//如果没有余数,那么每一组都是刚好能够保存的最大长度

int firstGroupLen = numDigits % digitsPerInt[radix];

if (firstGroupLen == 0)

firstGroupLen = digitsPerInt[radix];

//第一组数据存放到数组的最后一个

String group = val.substring(cursor, cursor += firstGroupLen);

magnitude[numWords - 1] = Integer.parseInt(group, radix);

if (magnitude[numWords - 1] < 0)

throw new NumberFormatException("Illegal digit");

 

// Process remaining digit groups

int superRadix = intRadix[radix];

int groupVal = 0;

while (cursor < len) {

group = val.substring(cursor, cursor += digitsPerInt[radix]);

groupVal = Integer.parseInt(group, radix);

if (groupVal < 0)

throw new NumberFormatException("Illegal digit");

// 这个方法是用来累计计算的,方法内部写的很复杂

//其实逻辑很简单,比如一个数字序列1234,求他表示的值是多少

// ( ( (1*10)+2 )*10+3 )*10 +4 = 1234

//这个方法就是用来计算的,只不过每一个位置是一个int 低32位当做数值 高32位当做进位

destructiveMulAdd(magnitude, superRadix, groupVal);

}

// Required for cases where the array was overallocated.

mag = trustedStripLeadingZeroInts(magnitude);

if (mag.length >= MAX_MAG_LENGTH) {

checkRange();

}

}

 

 

构造方法运行步骤

简单概括下这个方法:
前面的校验比较简单
1. 校验字符的合法性,并且获得符号位
2. 经过校验获取出来最终需要处理的字符的长度
然后就开始了计算
在正式计算之前,需要处理最高位,按照前面介绍的,能够表示的指定基数的最多位数进行划分
比如10进制表示9位,那么就是9个字符一组
先判断是否刚好整数倍? 
如果不是,比如10位,那么这个最高位这一个数字自己一组,剩下的9位一组,将会被放到两个int中
获得了最高位之后,就开始正式进行计算
如果还有字符需要处理的话
1. 按照位数进行截取,比如10进制截取9位
2. 截取后转换为数值,然后destructiveMulAdd  这个方法就是第一个参数的数,乘以第二个参数,然后加上第三个参数
就是这样一个过程
( ( (1*10)+2 )*10+3 )*10 +4 = 1234
每一次的循环中int数组的值都会发生变化
最终获得最后的结果
 

字符串构造方法计算示例

image_5bd81e9d_6ab0
 
使用字符串"-12345678986543215678901"  进行构造
我们按照方法的计算步骤走一下这个过程 
-12345678986543215678901
字符串总长度24
负号占1位, 光标移动一个位置 cursor=1
还有23个字符长度需要处理
需要处理的数字个数为
numDigits = len - cursor = 23
需要的二进制位数为
((numDigits * bitsPerDigit[radix]) >>> 10) + 1
(23*3402)/1024 +1 = 76+1 = 77
 
需要的int个数, 也就是数组长度为3
(int) (numBits + 31) >>> 5  (77+31)/32 = 3(3.375) 
十进制可以保存9位数字
23 不是9的倍数,商2 余数5
所以最高5位将会被截取单独存放
取前面5个数字,也就是12345
12345按照10进制解析为数字,存放到最后一个元素
也就是mag[2] = 12345   光标也跟随移动
数据显然没有处理结束, 进入循环处理, 直到最后结束
第一趟:
先获得接下来的9个字符 也就是 "678986543" ,然后解析为10进制数 678986543
此时
mag[0] = 0,mag[1] = 0  mag[2] = 12345
进入方法 destructiveMulAdd    destructiveMulAdd(int数组, 基数, 截取到的值)
他会乘以基数然后加上截取到的数字
image_5bd81e9d_439a
高32位进位,低32位作为得数
此时mag[0] 和mag[1] 不用在乘了,因为此时都是0  , mag[1] 加上进位即可
此时
mag[0]=0   mag[1] =2874     mag[2] 1263991296
还需要加上678986543
image_5bd81e9d_1d30
没有进位
所以第一趟结束之后,最终结果为
mag[0]=0   mag[1] =2874      1942977839
第二趟
获得接下来的9个字符 也就是 "215678901" ,然后解析为10进制数 215678901
image_5bd81e9d_3de2

低32位 为得数  高32位为计数  
也就是 
得数 -603122176  这是个有符号数
可以使用System.out.println(Integer.valueOf(0xDC0D1600)); 打印出来
进位  452384780
 
如同我们平时计算乘法一样,还需要计算前一位
 
此时  mag[0]=0   mag[1] =2874    mag[2] =  -603122176 
 
2874 x 10的9次方   =  2874000000000
加上 上一个进位  452384780
结果为 2874452384780
10 1001 1101    0100 0010 1011 0110 1001 1100 0000 1100
所以此时第二位得数为 1119263756(后32位)   
进位 10 1001 1101 (高32位)   669
 
所以此时mag数组为:
mag[0] = 669   mag[1] = 1119263756      mag[2]= -603122176
 
还需要加上最后截取的数值
image_5bd81e9d_41e
 
所以最终的结果为
mag[0]=669   mag[1] =1119263756      mag[2] =   -387443275
 
看起来很繁琐复杂,好奇害死猫,分析这么多只是为了更好地了解这一过程
如果没兴趣只需要记住BigInteger可以直接把字符串转换为数值进行存储就好了
 

其他构造方法

另外还有两个构造方法
public BigInteger(int bitLength, int certainty, Random rnd) 
          构造一个随机生成的正 BigInteger,它可能是一个具有指定 bitLength 的素数
public BigInteger(int numBits, Random rnd)
          构造一个随机生成的 BigInteger,它是在 0 到 (2numBits - 1)(包括)范围内均匀分布的值
 
 

方法简介

基础方法

获取符号位
signum()
常用数学函数
negate()   取负
abs()   绝对值
pow(int)  求幂
gcd(BigInteger)  最大公约数
min(BigInteger)  最小值
max(BigInteger) 最大值
四则运算与取整求余
add(BigInteger)  加法
subtract(BigInteger) 减法
multiply(BigInteger) 乘法
divide(BigInteger)  除法(取整)
remainder(BigInteger) 求余
divideAndRemainder(BigInteger)  取整和求余 返回的是一个数组
获取基本类型的值
不同于基本数值类型的包装类,此处并不是直接强转的
如果太大intValue 和 longValue 将分别返回低的32位和64位
longValue 和 doubleValue可能会被转换为无穷
intValue()
longValue()
floatValue()
doubleValue()
数值类型的准确值
longValueExact()
intValueExact()
shortValueExact()
byteValueExact()
所谓准确就是不会舍入或者转换,因为他们会进行数据长度的校验
否则将会抛出异常
比如
image_5bd81e9d_6661
位操作相关
and(BigInteger)  与 
or(BigInteger)   或 
not()    非
xor(BigInteger)   异或
andNot(BigInteger)   返回其值为 (this & ~val) 的 BigInteger 等效于 and(val.not())
shiftLeft(int) 左移
shiftRight(int)  右移 
 

取模与求余对比

计算过程相同
对于整型数a,b来说,取模运算或者求余运算的方法都是:
  1. 求 整数商: c = a/b;
  2. 计算模或者余数: r = a - c*b.
求模运算和求余运算在第一步不同
取余运算在取c的值时,向0 方向舍入;
取模运算在计算c的值时,向负无穷方向舍入;
 
因此,求模时结果的符号与b一致,求余时结果的符号与a一致
如果a,b都是正整数的话,求模与求余没有区别
mod(BigInteger)   
返回其值为 (this mod m) 的 BigInteger,取模不同于 remainder
 
BigInteger modPow(BigInteger exponent,BigInteger m)
image_5bd81e9d_56e2
 
BigInteger modInverse(BigInteger m)
image_5bd81e9d_5cf6
 

bitCount与bitLength

public int bitCount()
返回此 BigInteger 的二进制补码表示形式中与符号不同的位的数量
特别注意这个方法的含义
不是二进制补码表示形式的 1 位的数量,而是与符号不同的
bitLength
最小的二进制补码表示形式的位数,不包括 符号位
对于正 BigInteger,这等于常规二进制表示形式中的位数  就是去掉符号位占用的长度
 

valueOf(long)

valueOf(long)
包装一个long为BigInteger
BigInteger的valueOf有缓冲的作用
image_5bd81e9d_2ba7
 

equals(Object)

equals(Object)
重写了equals方法
数据相等 才是相等
image_5bd81e9d_25ba
 

toString hashCode CompareTo

public String toString(int radix) 转换为指定基数
toString()
hashCode()
compareTo(BigInteger)
小于、等于或大于 时,返回 -1,0,或 1
 

素数相关

是否素数
public boolean isProbablePrime(int certainty)
如果此 BigInteger 可能为素数,则返回 true,如果它一定为合数,则返回 false
如果 certainty <= 0,则返回 true
 
参数:
certainty - 调用方允许的不确定性的度量
如果该调用返回 true,则此 BigInteger 是素数的概率超出 (  1 - 1/(2的certainty次方)   )
此方法的执行时间与此参数的值是成比例的
返回:
如果此 BigInteger 可能为素数,则返回 true,如果它一定为合数,则返回 false
public static BigInteger probablePrime(int bitLength,
                                       Random rnd)
返回有可能是素数的、具有指定长度的正 BigInteger
此方法返回的 BigInteger 是合数的概率不超出 2的-100次方

参数:
bitLength - 返回的 BigInteger 的 bitLength。
rnd - 随机比特源,用这些随机比特选择用来进行质数测试的候选数
nextProbablePrime
public BigInteger nextProbablePrime()
返回大于此 BigInteger 的可能为素数的第一个整数
此方法返回的数是合数的概率不超出 2的-100次方
 

特殊的"位操作"

testBit(int)   计算 (this & (1<<n)) != 0
setBit(int)    计算  this | (1<<n)  
clearBit(int) 计算 this & ~(1<<n)
flipBit(int)    计算 this ^ (1<<n)
getLowestSetBit()
返回此 BigInteger 最右端(最低位)1 比特位的索引
也就是从最右边开始数找到的第一个1
此字节的右端开始到本字节中最右端 1 之间的 0 比特的位数
如果此 BigInteger 不包含1位,则返回 -1
计算 this==0? -1 : log2(this & -this)
 

toByteArray

public byte[] toByteArray()
BigInteger 内部使用int数组进行数据保存
一个int包含4个byte
BigInteger可以使用byte数组构造
也自然能够分解成byte数组进行保存
 

总结

要记住,内部的存储int数组 是final int[] mag;  所以是不可变的
他只是用来表示超出Java范围内的数值
本身的方法虽然内部细节特殊
但是外部呈现并没有什么特别的,只不过不能使用平时的+-*/符号,需要使用专门的方法
它提供了BigInteger大数值作为数值的基本运算的对应方法
并且还提供了java.lang.Math 的所有相关方法
另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作
 
posted @ 2018-10-30 17:19  noteless  阅读(14248)  评论(2编辑  收藏  举报