java biginteger 最大值_[十六]大数计算来啦~深入源码,手撕BigInteger计算过程!

点击上方 “ 布衣码农 ” ,免费订阅~选择“ 设为星标 ”,第一时间免费获得更新~

494bc43a5d2635e937e109e9ef72be82.png「布衣码农」一步一步教你BigInteger内部计算模式。 BigInteger和BigDecimal都是Java针对大数提供的类 超出了java的表示范围 10989b77b86a3ca2fcaf624db3057de8.png

属性简介

借助于signummag 来实现数据的符号位和实际数据的保存 final int signum 保存BigInteger的符号
负数-1
00
正数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进行构造的。 提供了两种形式的字节构造方法,可以指定符号的 a445948e111c3aed32c80cf33ee2ad75.png 使用字节进行构造,就是把所有的字节填充到int数组中, 不过要注意的是: 计算机中存储的数值都是补码的形式; 正数的补码与原码相同; 负数的补码是他的原码取反再加一; 就是把这些字节的补码按照顺序拼在一起,组合成int数组:
  • 如果是一个负数,会先得到真值的绝对值;

  • 如果有前导零,还会去掉所有的前导零;

而且, 是大端排序 ,大端排序,大端排序的把最终的数据存储起来 也就是说int数组中保存的都是真值的绝对值,使用专门的符号位记录正负和0。  
原码/反码/补码
先回顾下原码/反码/补码 的概念
原码符号位+数值位

符号位为0 表示正数,符号位为1 表示负数

数值位就是真值的绝对值

又被称为带符号的绝对值表示

反码正数的反码为其原码负数的反码为其原码的除符号位外,逐位取反
补码正数的补码为其原码负数的补码为其反码+1 
补码计算步骤
第一步求原码:先写出来她的原码--->符号位+数值位(绝对值)
第二步求反码:

如果是正数 反码与原码一样

如果是负数 反码为原码取反(除符号位外,逐位翻转) 

第三步求补码:

如果是正数 补码与原码一样

如果是负数 补码为反码 + 1

第四步扩充:

如果不足数据类型的宽度,将需要填充到指定宽度

符号位扩充,也就是正数补0  负数补1

总结:不管什么形式,第一位始终都是符号位,0 表示正数, 1表示负数正数原码/反码/补码 全都一样,知道一种就直接得到另外的形式负数如果知道补码,想要得到他的原码,只需要对补码再一次的求补码即可
示例1
8c3c6f307da35c2fc8bf9df8cab00fdc.png  
示例2
f1cef21f21ae5c44068b862026afc6e0.png   通过这两个例子应该可以看得出来,数值都是补码形式存放。 字节存储的也是补码 , int存储的也是补码, 所以使用字节构造 就是把所有的补码拼凑在一起就好了  。 拼凑排列好的补码,如果是正数,那么原码/反码/补码全都一样,存储的就是这个值; 如果是负数,还需要取他的绝对值,绝对值就是 再求一次补码,去掉符号位就是绝对值了; BigInteger数组中,存储的都是真值的绝对值的补码,真值绝对值得补码,其实就是原码去掉符号位嘛,一个意思。 就像上面的第二个例子  得到的补码为:   1000  0011  1111 0111  0000  0000  0101 1001 实际存储的是: 0111  1100   0000 1000  1111  1111 1010  0111

使用String构造

String作为参数的构造方法有两种形式, 本质上只是一种,那就是指定基数的字符串转换为BigInteger 简化形式就是默认十进制的形式 efb34513faeaea16c96081461395d766.png 通过String构造BigInteger的逻辑比较简单,但是实现看起来会有些令人模糊 接下来我们先介绍一点相关的计算基础
算法基础
int能够支撑的数据长度以及基数 我们知道,存储实际数据的是int数组 int表示范围是:    -231 ~ 231-1     也就是   -2147483648 ~ 2147483647 对于十进制 可以表示10位十进制数字 但是 2147483648 (2147483647+1)  仍旧是10位数字却溢出了 所以选择保存9位十进制数 所以每个int   十进制下最大值为10的9次方 92e6f153aca376786ffae38f4ba30406.png 对于二进制 最大值 231-1 ,所以只能保存30位 2进制数 所以每个int   二进制下最大值为2的30次方 35f145d585e4e3d7ce52136946481356.png 对于三进制 319 =1162261467 <2147483647<320 = 3486784401   所以能够保存19位 三进制数 所以每个int   三进制下最大值为3的19次方 20e45e1e0c6553c1642328baae2d6707.png 对于四进制 415 = 1073741824 < 2147483647 < 416 = 4294967296   所以能够保存15位 四进制数 所以每个int  四进制下最大值为4的15次方 d170e92c22bed61b06a940bbfe17fcad.png 对于十六进制167 =268435456 < < 2147483647 < 168 =  4294967296所以能够保存7位十六进制数所以每个int  十六进制下最大值为16的7次方c6934cf91c6e4e2a6164b9e38fc7b899.png 所以就有了这么两个映射数组 digitsPerInt 表示 每个int 可以表示的,指定进制下数字的位数,下标索引就是进制基数 比如可以表示十六进制的位数为digitsPerInt[16] = 7 intRadix 表示每个int可以表示的指定进制下的最大值,下标索引就是进制基数 比如 每一位int  可以表示的十进制的最大值为  intRadix[10] = 0x3b9aca00=1,000,000,000 c80c97b5d5939b9b3474ca55f667a6b4.png 其实 intRadix 这个数就是: BigInteger在这个基数下的基数 这句话有点绕,BigInteger内部是数组,假如为mag[0] mag[1]    intRadix[10] = 0x3b9aca00 那么也就是,BigInteger在十进制,也就是10为基数下的基数为0x3b9aca00 那么这个值就是 mag[0] x 0x3b9aca00 1    + mag[1] x 0x3b9aca00 0   就如同十进制的数12的计算方式为1x10 1  + 2 x10 0  =12 一样的道理 下面还会继续说明 同理它 内部也有两个针对Long 的数组 ,用于内部计算使用
BigInteger内部使用int数组表示普通数值使用每个数值位上的数字进行表示
一个BigInteger有多个int一个普通数值有多个数字位
每个int能够表示的指定进制的最大值--intRadix 中保存的数据 其实 就是 BigInteger 的基于每个int作为一个元素的进制基数 93ea271c93f9fdeb2c0d0cda8373ca70.png 假设R为指定的基数,L为指定基数的数字的长度,那么用多少位2进制数可以表示? x位二进制能够表示的最大值为: 85331d929b79b0f2408d833d4a71364a.png L位R进制的数能够表示的最大值为:881046c9b8e371e2a30fe647a3bd5c85.png 比如R=10 L=2 也就是十进制两位数能够表示的最大值为: 10的平方减1     等于 99 2675f33a3eb565b18b1f61ff7e0e5138.png 解上面的方程,可以得出来, x的长度为 :
L  乘以  以2为底R的对数
内部还有一个数组, 这个数组的值就是以2为底R的对数的值,然后乘以1024,然后进行向上取整 74285470aa27714c5781ef6490e94372.png bitsPerDigit 就是每个数字需要的比特位数乘以1024后在取整。 之所以乘以1024然后在取整, 应该是为了简化运算,这个数必然要是2的N次方,计算机移位最快。 当然,这个地方乘以1024 实际使用的时候必然也还得除以1024。 1b799cc9a3546b00b39069206e695f16.png 以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 0d69aca6fb3d9e9f10689a76af8fecb5.png 此时 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 signint 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之后变成了取整了,然后再加上一,百分百够用,需要的比特位数保存到numBitslong numBits = ((numDigits * bitsPerDigit[radix]) >>> 10) + 1;if (numBits + 31 >= (1L << 32)) {reportOverflow();}//numWords 记录的是实际需要的int类型数据的个数,也就是数组的长度//右移5位就是除以32 就是计算数组的长度,除法会取整,防止1个不足32位的时候,就会变成0了所以numBits加上31 之后再除以32int 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 groupsint 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数组的值都会发生变化, 最终获得最后的结果  
字符串构造方法计算示例
05c7a9d2180e42f56590aeeddcc0ed14.png   使用字符串"-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数组, 基数, 截取到的值)
他会乘以基数然后加上截取到的数字e26c8284ef14963f4f318af6aa6acd1f.png高32位进位,低32位作为得数
此时mag[0] 和mag[1] 不用在乘了,因为此时都是0  , mag[1] 加上进位即可
此时
mag[0]=0   mag[1] =2874     mag[2] 1263991296
还需要加上678986543c6cad04bbcf1e7a2e0685cb62531684a.png没有进位
所以第一趟结束之后,最终结果为
mag[0]=0   mag[1] =2874      1942977839 第二趟
获得接下来的9个字符 也就是 "215678901" ,然后解析为10进制数 21567890175629d9653d207b670573c63f65afb76.png 低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   还需要加上最后截取的数值 c443c01222b5d2790922bf543a8cdb2d.png   所以最终的结果为 mag[0]=669   mag[1] =1119263756      mag[2] =   -387443275 看起来很繁琐复杂,好奇害死猫,分析这么多只是为了更好地了解这一过程, 如果没兴趣只需要记住BigInteger可以直接把字符串转换为数值进行存储就好了,否则,这可能是你Java生涯的一篇劝退文~
其他构造方法
另外还有两个构造方法
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()
所谓准确就是不会舍入或者转换,因为他们会进行数据长度的校验 否则将会抛出异常 比如

624e64ce5b917b15da4a90578cfe1b55.png

位操作相关

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) 2162bc708db372ff05796e1b1f4f0a6e.png   BigInteger modInverse(BigInteger m) 17a19fabb65b8a90c9b5741a2015ac61.png
bitCount与bitLength
public int bitCount()

返回此 BigInteger 的二进制补码表示形式中与符号不同的位的数量特别注意这个方法的含义不是二进制补码表示形式的 1 位的数量,而是与符号不同的

bitLength最小的二进制补码表示形式的位数,不包括 符号位对于正 BigInteger,这等于常规二进制表示形式中的位数  就是去掉符号位占用的长度

valueOf(long)
valueOf(long)

包装一个long为BigInteger,BigInteger的valueOf有缓冲的作用

dce958b1c8102acfbd47c2eac4d70f13.png

equals(Object)
equals(Object)

重写了equals方法,数据相等 才是相等

ee73527ef5827f2d1bdb636d6e685b9c.png

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 - 随机比特源,用这些随机比特选择用来进行质数测试的候选数
public BigInteger nextProbablePrime()
返回大于此 BigInteger 的可能为素数的第一个整数 此方法返回的数是合数的概率不超出 2的-100次方
特殊的"位操作"
testBit(int);//计算 (this & (1<setBit(int);//计算  this | (1<clearBit(int);//计算 this & ~(1<flipBit(int);// 计算 this ^ (1<
getLowestSetBit()
返回此 BigInteger 最右端(最低位)1 比特位的索引 也就是从最右边开始数找到的第一个1 此字节的右端开始到本字节中最右端 1 之间的 0 比特的位数 如果此 BigInteger 不包含1位,则返回 -1 计算 this==0? -1 : log2(this & -this)
toByteArray
public byte[] toByteArray()

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

总结

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

··················END··················

注:非技术讲解配图均来源于网络

期待分享

如果对你有用

可以点个 「在看」 或者分享到 「 朋友圈 」 哦

d55efa06a1c1eb630776c44f56384df8.png你「在看」吗? e0696836fc14399e448c96430907e05d.gif↓↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值