我对python大整数的实现,点击查看
- 在之前的文章,我实现过Python的大整数,可以说,在python中是没有数值类型这一说的, 你可以为一个变量赋值为上千亿,也不会出错。 没有了 如java中的
int, short, byte
等对位数的限制。已知int由四个字节表示,那么就有4X8 = 32位 可以表示的范围就是-2^31 到 2^31 -1
。但是在Python中
# 参考大整数的实现,Python中的十进制是没有限制的。
>>> a = 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000
>>> a
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Python中的二进制
- 那么问题来了,既然十进制没有限制,那么二进制有没有限制呢? 答案是
没有
- 首先要知道,在计算机中,数值都是以二进制形式保存的,所以任何编程时输入的10进制,都是转化为 2进制运算。
整数不变(整数补码等于源码),负数则是利用其补码。
# 打印a的二进制形式,可以看到根本没有位数限制
>>> bin(a)
'0b1010010010111000110010101011000110100001010101100011111101010010010101110111000000000001101110001001000100011000010110010011100010001010011110100100100011101001101010111000011000111111011001111101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
- 理解这些之后,就可以往下推论了,既然没有位数限制, 那也就是
不存在溢出的情况!python的数值类型不存在溢出的情况
。 像是java语言,当计算两个int数的和大于 int32位所能表示的时候,就会被截断!只要32位,那么就会返回错误的结果
。
在java中:
// 2^31 - 1
int a = 2147483647;
int b = 21474;
System.out.println(a + b);
得到结果将是
-2147462175
接着看,为什么int进制的表示是 -2^31 到 2^31 -1?
举个简单的例子,以4位二进制为例子:
4 位的:最高一位表示符号位
表示正数:
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
范围是 0 --> 2^3-1 也就是0 --> 7
负数的表示,原码: 但是在计算机中存储的都是补码形式:补码中用(-128)代替了(-0)!
原码取反 + 1 --> 补码
1000 -0(-8)(第一个1既是符号位也可以表示数值位) 1111 + 1 --> 1000
1001 -1 1110 + 1 --> 1111
1010 -2 1101 + 1 --> 1110
1011 -3 1100 + 1 --> 1101
1100 -4 1011 + 1 --> 1100
1101 -5 1010 + 1 --> 1011
1110 -6 1001 + 1 --> 1010
1111 -7 1000 + 1 --> 1001
反码
所以负数的表示是: -1 --> -2^3 也就是 -1 --> -8
总体表示范围是 -2^3 -- 2^3 - 1
- 这就解释了 32位 int 类型表示数的范围了~
之后看一道位运算的算法题
力扣:371号算法题: 两整数之和
- 这道算法题的本质,就是不希望你使用+号,而是了解计算机当中如何使用位运算去求 两个二进制数的 加法和。
计算机组成原理中是有讲到的
。 - 思路:
举个例子假如说 4 + 5(正负数规律是一样的)
因为4 和 5的值比较小,方便计算,我们表示成 4位二进制即可
4: 0100
5: 0101
1. 在不考虑进位的条件下: 4 + 5:
0100
0101
------
0001
1 + 1 = 0 ; 1 + 0 = 1; 0 + 0 = 0 恰好符合 异或的原则(同出0,异出1)
结论:异或运算就相当于是 无进位的加法。
2. 如何计算进位:
先看看进位是多少, 我们相加得到进位放到下面
0100
0101
------
0100 # 这个数字表示该位计算后得到的进位
1 + 1 进位是 1; 1 + 0 和 0 + 0 进位是 0,恰好符合 按位&的原则。
3. 但是进位是进到下一位的!,所以再执行 左移运算 <<
进位: 0100 左移
0100 << 1 变为 1000
此时左移后的进位 再 和 异或的无进位结果相加,就是 + 法运算的结果了
但是! 事情没这么简单,这个进位 和 异或的无进位结果相加 可能仍然产生进位,所以要不断循环这个过程,当进位为 0 时, 异或运算的无进位结果 就是正确的结果!。 就像 十进制中 3 + 4 = 7 一样,不需要考虑进位,结果是正确的,因为没有进位。
进位数值 加上 无进位加法计算的数值,就是最终结果( 所以要保证最终的计算,不再产生进位,那么无进位加法的结果就是正确的!)
- 这就是整个进位的计算过程,下面上Python代码展示:
- 代码十分简洁,a , b 表示加数,被加数, carry表示进位,当carry为0时停止(b是被加数,也是进位,因为进位 != 0, 还要加起来)
def getSum(self, a: int, b: int) -> int:
"""
:type a: int
:type b: int
:rtype: int
"""
while b != 0:
# 计算进位
carry = (a & b) << 1
# 计算两数无进位和
a = (a ^ b)
b = carry
return a
- 分析上述过程就会发现一个致命错误!
假设输入测试用例, 1, -1,调用函数getSum(1, -1)
# 因为 数字较小,我们还是用 4位二进制进行模拟
1: 0001
-1: 1111
(1)
计算进位是否为0: (-1 & 1) << 1 = 2
0001 &
1111
-------------
0001 << 1 得:0010 结果10进制为: 2
计算无进位和: (-1 ^ 1) << 1 = -2
0001 ^
1111
-------------
1110 十进制为-2(和)
(2) 结果余数不为0,继续:
计算进位是否为0: (-2 & 2) << 1 = 4
1110 &
0010
-------------
0010 << 1 得:0100 结果10进制为: 4
计算无进位和: -2 ^ 2 = -2
1110 ^
0010
-------------
1100 十进制为-4(和)
(3)余数不为0继续
计算进位是否为0: (-4 & 4) << 1 = -8
1100 &
0100
-------------
0100 << 1 得:1000 结果10进制为: -8 (Python计算就开始出错了(按照4位二进制来说),结果会为8)
计算无进位和: -4 ^ 4 = -8
1100 ^
0100
-------------
1000 十进制为-8(和)
(关键一步到了!)
(4)余数不为0继续
计算进位是否为0: (-8 & -8) << 1 = 16( 正确的四位2进制,结果应该是 0!)
1000 &
1000
-------------
1000 << 1 注意!!!! 得:10000 结果10进制为: 16 (这里出现问题),本来应该去0的,但是确得到了 16,变为了 5位2进制表示。这就是Python特殊的地方! 无限制表示二进制数。 我们还假设得到的二进制数是0
(此时进位已经变为 0, 得到的最后异或结果(无进位加法),就是最终正确结果!)
计算无进位和: -8 ^ -8 = 0
1000 ^
1000
-------------
0000 十进制为0(和)
结论: 程序返回,得出最终正确结果为 0!!!
但是上面Python中的问题, << 左移 会无限增大一个二进制数,没有位数的限制(即使我们上面假设了4位2进制),
所以数字运算永不为0,永远不会得到结果!! 这就是Python在处理一些 位运算时会产生的问题, 因为没有明确的 2进制表示位数限制。
- 解决: 就需要模拟带有 位数的限制。
- 理解位数的意义: 假设32位 2进制(int)。 那么只要是数值运算结果数值不超过32位,都是正确的。 如果是16位的,那么16位以内的数值运算结果,也都是正确的,因为在计算机中存储就是以16位存储,没有多余的位数。
通过掩码,模拟32位整数(只存储32位2进制)
,举个例子:
>>> bin(123123123123123)
'0b11011111111101011010110000001000111001110110011'
我们只要32位,就要设立一个掩码: mask = 0xFFFFFFFF (16进制表示,32位1)
& mask:
110111111111010 11010110000001000111001110110011
000000000000000 11111111111111111111111111111111 &
-------------------------------------------------
000000000000000 11010110000001000111001110110011
运算结果恰好是取后32位, 当一个二进制位数超过限制时,我们只要后32位。
要注意Python中两个数进行位运算,本身位数是不固定的,但是 哪个长,就以哪个为准,举例子:
>>> 1 ^ -1
-2
# 结果为 -2 ,可能是
01
11
-----
10 --> 就是 -2
>>> (1 ^ -1) & 0xFFFFFFFF
4294967294
运算
1111 1111 1111 1111 1111 1111 1111 1110 (-2)-2的表示根据 0xFFFFFFFF的长度变长表示
1111 1111 1111 1111 1111 1111 1111 1111 (0xFFFFFFFF)
-----------------------------------------
1111 1111 1111 1111 1111 1111 1111 1110 (这个应该表示的是 -2的补码形式,但是python返回的,直接把他当做整数返回4294967294)
>>> bin(4294967294)
'0b11111111111111111111111111111110'
一样,验证了推论!
要想再得到原来的2,就要将这个反码数,转换为原码让Python输出
>>> ~(4294967294 ^ 0xFFFFFFFF)
-2
~(a ^ mask) 将a表示的补码转换为原码输出,正数不需要改变。
>>> 2 & 0xFFFFFFFF
2
所以如下解:
def getSum(self, a: int, b: int) -> int:
# 定义32位最大整数 : [0111 1111 1111 1111 1111 1111 1111 1111]
MAX = 0x7FFFFFFF
# 定义获取后32位的掩码
mask = 0xFFFFFFFF
while b != 0:
carry = ((a & b) << 1) & mask # 得到32位长,正确进位
a = (a ^ b) & mask # 异或结果也是 32位长
b = carry
# 如果 a < MAX 说明是个整数, a > MAX, 那上面的第一位一定是1(因为和掩码进行运算,都变为32位长的表示),就代表负数, ~(a ^ mask): 将a的补码转成负数
return a if a <= MAX else ~(a ^ mask)
- 就此结束,得到正确结果!