美文网首页
我不知道的按位运算

我不知道的按位运算

作者: JasonJe | 来源:发表于2019-01-06 17:08 被阅读0次

    💻计算机为什么会加减乘除?笔者是一直都没了解过,认为加减乘除就是理所当然的事情,但计算机中的万物皆为0、1,笔者的认为是不可能那么简单的。

    其实说直接点,就是计算机的原理是什么?

    1. 二进制

    二进制就是计算机使用的“语言”。简单举例来说就是:人类使用十进制来计数,它的运算规则是“逢十进一”。二进制就是“逢二进一”的运算规则。

    为什么计算机使用二进制呢?这个可以归结于一个人有十根手指,所以人类普遍认知是使用十进制。计算机是由一大堆电子电路组成的,使用二进制正好对应电路里的高低电平(大部分电子器件只有两种状态),这就好比计算机只有“两根手指”。

    1.1 原码

    原码是二进制的一种表现形式。其为一个整数绝对值的二进制,加上符号位(0为正,1为负)。

    整数 绝对值 绝对值的二进制 原码
    +3 3 000 0011 0000 00011
    +3 3 000 0011 1000 00011
    • 原码是给人看的二进制,不是计算机保存的,不直接参与计算。

    1.2 反码

    针对负数做处理,在原码基础上,除了符号位,其它位取反。

    整数 绝对值的二进制 原码 反码
    +3 000 0011 0000 00011 0000 00011
    +3 000 0011 1000 00011 1111 11100
    • 反码是在计算机存储的二进制,但不是真正的二进制值,它不直接参与计算。

    1.3 补码

    补码是真正的二进制值,主要针对负数做处理,在反码的基础上加1。

    整数 原码 反码 补码
    +3 0000 00011 0000 00011 0000 00011
    +3 1000 00011 1111 11100 1111 11101

    1.4 理解补码

    假如一个时钟现在显示的是10点钟,如何将它调到6点钟?

    解:有两种方法,一是向后拨8个小时,二是向前拨4个小时

    在这个例子中,8 和 4 互为补数,也就是说4的补码是8,8的补码是4,而这个时钟的模就是12

    注意:可能有人会想,在往后调8个小时虽然也调到了6点,但是他实际上比原来日期多了12小时。是的,的确如此,但是你的时钟有地方存储了这多余的12个小时吗?答案是没有,所以在你调完后,你没有记录这12个小时,换句话说,你把这溢出的12个小时自动舍弃了。当第二个人来查看闹钟时间的时候,他看到的时间就是准的。

    二进制补码表示负数是最方便的方式,它的便利体现在,所有的加法运算(加正数、加负数)可以使用同一种电路完成。

    我们以-8作为例子。

    假定有两种表示方法。一种是直觉表示法,即10001000;另一种是2的补码表示法,即11111000。请问哪一种表示法在加法运算中更方便?

    随便写一个计算式,16 + (-8) = ?其中,16的二进制表示是 00010000,-8的二进制表示是 10001000

    • 直觉表示法:
     00010000
    +10001000
    ---------
     10011000
    

    可以看到,如果按照正常的加法规则,就会得到10011000的结果,转成十进制就是-24。显然,这是错误的答案。也就是说,在这种情况下,正常的加法规则不适用于正数与负数的加法,因此必须制定两套运算规则,一套用于正数加正数,还有一套用于正数加负数。从电路上说,就是必须为加法运算做两种电路。

    • 补码表示法:
     00010000
    +11111000
    ---------
    100001000
    

    可以看到,按照正常的加法规则,得到的结果是100001000。注意,这是一个9位的二进制数。我们已经假定这是一台8位机,因此最高的第9位是一个溢出位,会被自动舍去。所以,结果就变成了00001000,转成十进制正好是8,也就是16 + (-8) 的正确答案。这说明了,2的补码表示法可以将加法运算规则,扩展到整个整数集,从而用一套电路就可以实现全部整数的加法。

    2. 按位运算

    Python为例,常用位运算符包括以下6中:

    • 按位与 &
    • 按位或 |
    • 按位异或 ^
    • 按位翻转 ~
    • 左移运算 <<
    • 右移运算 >>

    2.1 按位与(&)

    即按照对应位置的二进制进行 and运算,其中运算规则为 1 & 1 = 11 & 0 = 00 & 1 = 00 & 0 = 0

    下面的 5 & 3示意如下:

    十进制     二进制
    5 --> 0000 0101
      &        &&&&
    3 --> 0000 0011
      =        ||||
    1 <-- 0000 0001
    
    • 位运算判断奇偶性

    n&1 == 1 # 奇数返回1,偶数返回0

    2.2 按位或(|)

    即按照对应位置的二进制进行 or运算,其中的运算规则为 1 | 1 = 11 | 0 = 10 | 1 = 10 | 0 = 0

    下面的 5 | 3示意如下:

    十进制     二进制
    5 --> 0000 0101
      |        ||||
    3 --> 0000 0011
      =        ||||
    7 <-- 0000 0111
    
    • 向上取奇数

    map(lambda x:x|1, range(6)) # 结果为[1, 1, 3, 3, 5, 5]

    2.3 按位异或(^)

    即按照对应位置的二进制进行 xor运算,其中的运算规则为 1 ^ 1 = 01 ^ 0 = 10 ^ 1 = 10 ^ 0 = 0

    下面的 5 ^ 3示意如下:

    十进制     二进制
    5 --> 0000 0101
      ^        ^^^^
    3 --> 0000 0011
      =        ||||
    6 <-- 0000 0110
    
    • 只出现一次的数字

    reduce(lambda x,y: x^y, [1,1,3,2,4,3,4]) # 累积进行异或运算,找出只出现一次的数字为2,出现两次的数字就抵消掉了

    2.4 按位翻转(~)

    即1变成0,0变成1。

    十进制     二进制
    5 --> 0000 0101
      ~   ~~~~ ~~~~
    -6<-- 1111 1010
    

    翻转相当于跟 -1做异或运算,即~n = n ^ (-1)

    十进制     二进制
    5 --> 0000 0101
      ^        ^^^^
    -1--> 1111 1111
      =        ||||
    6 <-- 1111 1010
    

    2.5 左移运算(<<)

    左移运算是将二进制数值整体向左边移动n个位置,空出来的位置补0。

    下面的 5 << 2示意如下:

    十进制     二进制
    5 --> 0000 0101
      <<    << 2
    20<-- 0001 0100
    
    • 用于乘法计算
      5 * 7
    = (101 * 111)_2
    = (101 * (100 + 10 + 1))_2
    = (101 * 100 + 101 * 10 + 101 * 1)_2
    = 5 << 2 + 5 << 1 + 5 << 0
    = (10100 + 01010 + 00101)_2
    = 20 + 10 + 5
    = 35
    

    2.6 右移运算(>>)

    左移运算是将二进制数值整体向右边移动n个位置,空出来的位置补上符号位的数值。即正数补0,负数补1。

    下面的 5 >> 2-5 >> 2示意如下:

    十进制     二进制
    5 --> 0000 0101
      >>    >> 2
    1 <-- 0000 0001
    
    十进制     二进制
    -5--> 1111 1011
      >>    >> 2
    -1<-- 1111 1110
    
    • 用于除法
      5 / 3
    = 5 >> 2
    = (101 / 100)_2
    = (001)_2
    = 1
    

    2.7 位运算实现加减乘除

    def add(num1, num2):
        while num2 != 0:
            temp = num1 ^ num2
            num2 = (num1 & num2) << 1
            num1 = temp
        return min(max(-2147483648, num1), 2147483647)
    
    def sub(num1, num2):
        return add(num1, add(~num2, 1))
    
    def mul(num1, num2):
        sign = (num1 > 0) is (num2 > 0)
    
        if num1 < 0:
            num1 = add(~num1, 1)
        if num2 < 0:
            num2 = add(~num2, 1)
    
        result = 0
        while num2:
            if num2 & 1:
                result = add(result, num1)
            num1 = num1 << 1
            num2 = num2 >> 1
    
        if not sign:
            result = - result
        return result
    
    def div(num1, num2):
        sign = (num1 > 0) is (num2 > 0)
    
        if num1 < 0:
            num1 = add(~num1, 1)
        if num2 < 0:
            num2 = add(~num2, 1)
    
        result = 0
        while (num1 >= num2):
            tmp, i = num2, 1
            n = 4
            while(num1 >= tmp):
                num1 -= tmp
                result += i
    
                tmp = tmp<<n
                i = i << n
            n = n >> 2
    
        if not sign:
            result = -result
    
        return min(max(-2147483648, result), 2147483647)
    

    在早期版本中,如Python2.7,整数的有int和long两个类型。int类型是一个固定位数的数;long则是一个理论上可以存储无限大数的数据类型。当数大到可能溢出时,为了避免溢出,Python会把int转化为long。

    而Python3.x之后整数只有一个可以放任意大数的int了。可是无论哪种,都是采用了特殊的方法实现了不会溢出的大整数。

    在进行负数的按位加法时,有可能发生在最高位还要向前进一位的情形,正常来说,这种进位因为超出了一个int可以表示的最大位数,应该舍去才能得到正确的结果。但在Python中因为上述原因会产生一个不会溢出的大整数,所以在进行负数的按位加法时,结果会不断变大。

    这个问题在 Java、C、C++ 中不会存在,这也是Python效率低的一个原因。

    相关文章

      网友评论

          本文标题:我不知道的按位运算

          本文链接:https://www.haomeiwen.com/subject/qkwsrqtx.html