详细讲解:Python中二进制以及数值的特殊性,在位运算中需要注意的点

我对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(正负数规律是一样的)
因为45的值比较小,方便计算,我们表示成 4位二进制即可
40100
50101

1. 在不考虑进位的条件下: 4 + 50100
	0101
	------
	0001
	
	1 + 1 = 0 ; 1 + 0 = 1; 0 + 0 = 0 恰好符合 异或的原则(同出0,异出1)
结论:异或运算就相当于是 无进位的加法。 

2. 如何计算进位:
先看看进位是多少, 我们相加得到进位放到下面
	0100
	0101
	------
	0100   # 这个数字表示该位计算后得到的进位
	
	1 + 1 进位是 11 + 00 + 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位二进制进行模拟
10001
-111111)
计算进位是否为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,变为了 52进制表示。这就是Python特殊的地方! 无限制表示二进制数。  我们还假设得到的二进制数是0

(此时进位已经变为 0, 得到的最后异或结果(无进位加法),就是最终正确结果!)
计算无进位和: -8 ^ -8 = 0
	1000 ^
	1000
	-------------
	0000   十进制为0(和)
	
结论: 程序返回,得出最终正确结果为 0!!!

但是上面Python中的问题, << 左移 会无限增大一个二进制数,没有位数的限制(即使我们上面假设了42进制),
所以数字运算永不为0,永远不会得到结果!! 这就是Python在处理一些 位运算时会产生的问题, 因为没有明确的 2进制表示位数限制。

  • 解决: 就需要模拟带有 位数的限制。
  • 理解位数的意义: 假设32位 2进制(int)。 那么只要是数值运算结果数值不超过32位,都是正确的。 如果是16位的,那么16位以内的数值运算结果,也都是正确的,因为在计算机中存储就是以16位存储,没有多余的位数。
  • 通过掩码,模拟32位整数(只存储32位2进制),举个例子:
>>> bin(123123123123123)
'0b11011111111101011010110000001000111001110110011'
我们只要32位,就要设立一个掩码: mask = 0xFFFFFFFF16进制表示,321& 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 11110xFFFFFFFF-----------------------------------------
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)
  • 就此结束,得到正确结果!
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值