Loading

一篇读懂Python中的位运算

什么是位运算?

简单来说,位运算是把数字转换为机器语言,也就是二进制来进行计算的一种运算形式。
在古老的微处理上,位运算比加减运算略快,要比乘除运算快的多。虽然现在随着技术的迭代,新的架构在推陈出新,位运算与加减法相差无几,但是仍然快于乘除运算。为什么这么说呢?因为位运算符直接处理每一个比特位(bit),这么底层的运算,当然快了!但是缺点也很明显,理解起来稍显复杂,不够直观。这在许多的场合都不使用它们, 因为它会增加代码难度和排错!不利于粘贴和复制(纯属扯淡!)。
首先,我们要明白一点,位运算符之对整数起作用,如果一个操作数(如浮点数)不是整数,那它首先会自动转换为整数后再执行。另外Python语言参考中也强烈表示只能是整数!


我想,你可能要试试看:

>>> c = 1.2
>>> d = 3.5
>>> c | d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'float' and 'float'

要想玩明白位运算,我们要回顾一些基础知识。

用到的基础知识

问题来了:计算机内部是如何用二进制表示整数的

让我们思考一个问题,计算机内部是如何用二进制表示这些整数的?计算机内用定点数表示的。那问题又来了,什么是定点数?
在计算机内,定点数有3种表示法:原码、反码和补码。
问题一个接着一个,原码、反码、补码又是什么鬼东西?妈妈呀,还让我回去搬砖吧!

原码

原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小。
口诀在此:一个正数,转换为二进制位就是这个正数的原码。负数的绝对值转换成二进制位然后在高位补1就是这个负数的原码。
比如说int类型的3的原码是11B(B表示二进制位,这里你可以多了解一些进制之间的转换),在32为机器上占4个字节,所以,高位补0就是:

00000000 00000000 00000000 00000011	# 一个字节8个bit位

那么int类型的-3的绝对值的二进制位就是11B展开后最高位补1就是:

10000000 00000000 00000000 00000011

但是呢,原码也有缺点,原码中的0分为+0-0。不仅如此,在进行不同符号的加法运算或者同符号的减法运算时,不能直接判断出结果的正负,我们必须要将两个值的绝对值进行比较。然后再进行加减操作。最后符号由绝对值大的决定,于是乎,下面有请反码登场。

反码

反码是数值存储的一种,多应用于系统环境设置,如linux平台的目录和文件的默认权限的设置umask,就是使用反码原理。
口诀不能忘:正数的反码就是原码,负数的反码等于原码除符号位以外所有位取反。
比如还是刚才的那个int类型的3的反码是:

00000000 00000000 00000000 00000011	 # 正数的反码就是原码,这么写没毛病

int类型的-3的反码是,让我们默念公式:负数的反码等于原码除符号位以外所有位取反!

10000000 00000000 00000000 00000011	 # -3的原码
11111111 11111111 11111111 11111100	 # 最高位为符号位,不变,其余取反

这样,反码解决了加减法运算问题(不理解就自己再查吧),我们就该着手处理+0-0的问题了,有请补码上台领奖!

补码

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路支持。
记住口诀:正数的补码与原码相同,负数的补码为其原码除符号位外所有位取反(这就是反码了),然后最低位加1。
还是那个int类型的3的补码是:

00000000 00000000 00000000 00000011	 # 正数的补码与原码一致

那么int类型的-3的补码就是,让我们手掐口诀:

10000000 00000000 00000000 00000011  # -3的原码
11111111 11111111 11111111 11111100	 # 负数的补码为其原码除符号位外所有位取反
11111111 11111111 11111111 11111101  # 然后最低位加1,完美!

原、反、补码小结

原、反、补码小结:

  • 正数的反码和补码都与原码相同
  • 负数的反码为该数的原码除符号位外所有位取反
  • 负数的补码为该数的原码除符号位外所有位取反,然后最低位加1

优缺点:

  • 原码最好理解,但是存在加减法运算不方便的问题,还有俩零蛋捣乱
  • 反码稍微难点,但仅解决了加减法的问题。俩零继续捣乱
  • 补码理解相对困难,但解决了上面的俩问题

单、双、三目运算

根据操作数的个数,运算符可以分为单目、双目、三目运算符,也称为一元、二元、三元运算符等。若完成一个操作需要两个操作数,则称该运算符为双目运算符;若完成一个操作需要一个操作数,则称该运算符为单目运算符。

Python中的按位运算

我们在之前的原、反、补码中了解了基本的数值存储。那么这里就开始具体的学习Python中按位是怎么运算的,首先来看规则。Python中的按位运算规则如下表所示:

运算符 描述
& 按位运算符,参与运算的两个值,如果相应位都为1,则该位的结果为1,否则为0
^ 按位异或运算符,当两个对应的二进位相异时,结果为1
~ 按位取反运算符,对数据的每个二进制位取反,即把1变为0,把0变为1
| 按位或运算,只要对应两个二进制位有一个为1时,结果就为1
<< 左移动运算符:运算数的各二进制位全部左移若干位,由 << 右边的数字指定了移动的位数,高位丢弃,低位补0
>> 右移动运算符:把>>左边的运算数的各二进制位全部右移若干位,>> 右边的数字指定了移动的位数

在Python的按位运算符中,只有反转~运算符是单目运算,其余都是双目运算。

按位与 &

按位与的规则是:参与运算的两个值,如果相应位都为1,则该位的结果为1,否则为0,也就是说:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 1 = 0
  • 0 & 0 = 0

我们首先来看一个示例:

>>> 3 & 5
1

分析,我们首先来看它们各自的补码,我们接下来的演示只用一个字节8位表示就行,32位太长了(搞起来难受):

0000 0011  # 3的补码
0000 0101  # 5的补码
0000 0001  # 根据按位与的规则,得出补码结果

得出的结果是补码类型的, 我们要先把补码转换为原码,再将二进制转换为十进制的结果。正数的补码等于原码,所以结果就是1
再来个示例:

>>> -2 & -3
-4

老套路,先找各自的补码,再求结果:

1111 1110	# -2的补码
1111 1101	# -3的补码
1111 1100	# 结果

我们将补码转换为原码,默念口诀:补码转原码,符号位不变,数值为按位取反,末位加1:

1111 1100	# 补码
1000 0011	# 符号位不变,数值位按位取反
1000 0100	# 末位加1

想着最高位的符号位为负,二进制100对应的十进制是4,最终结果就是-4
再来个例子:

>>> -2 & 3
2

老套路,拿到它们各自的补码,再求结果:

1111 1110	 # -2的补码
0000 0011	 # 3的补码
0000 0010	 # 结果

找到对应的十进制是2
小结:在按位与的结果中,只有是负数的情况下,才需要将补码转换为原码,然后再求对应的十进制数。

按位或 |

先把口诀放这里:按位或运算,只要对应两个二进制位有一个为1时,结果就为1。也就是说:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 1 = 1
  • 0 | 0 = 0

再把例子拿过来:

>>> 3 | 5
7

拿到补码:

0000 0011 	# 3的补码
0000 0101	# 5的补码
0000 0111	# 结果

二进制的111转为十进制是7
再来个例子:

>>> -2 | -3
-1

各自的补码是:

1111 1110	 # -2的补码
1111 1101	 # -3的补码
1111 1111	 # 结果

拿到了结果,我们还需要将补码转换为原码再转10进制:

1111 1111	# 结果
1000 0000	# 高位不变,其余取反
1000 0001	# 末位加1

最高位的是负号,最终的结果是-1
再来个例子:

>>> -2 | 3
-1

老套路,拿到它们各自的补码,再求结果:

1111 1110	# -2的补码
0000 0011	# 3的补码
1111 1111	# 结果

继续补码转原码再转十进制:

1111 1111	# 结果
1000 0000	# 高位不变,其余取反
1000 0001	# 末位加1

最高位为负号,找到对应的十进制是-1

按位异或 ^

先把规则列出来:按位异或运算符,当两个对应的二进位相异时,结果为1,也就是说:

  • 1 ^ 1 = 0
  • 1 ^ 0 = 1
  • 0 ^ 1 = 1
  • 0 ^ 0 = 0

再把例子拿过来:

>>> 3 ^ 5
6

拿到补码:

0000 0011 	# 3的补码
0000 0101	# 5的补码
0000 0110	# 结果,注意按照规则来

正整数的结果一目了然,二进制的110转为十进制是6
再来个例子:

>>> -2 ^ -3
3

各自的补码是:

1111 1110	# -2的补码
1111 1101	# -3的补码
0000 0011	# 结果

首先来看011对应的十进制是3,所以最终结果是3
再来个例子:

>>> -2 ^ 3
-3

老套路,拿到它们各自的补码,再求结果:

1111 1110	# -2的补码
0000 0011	# 3的补码
1111 1101	# 结果

结果是负数,只能将补码转原码再转10进制了:

1111 1101	# 结果
1000 0010	# 高位不变,其余取反
1000 0101	# 末位加1

最高位为负号,二进制的1013, 所以对应的十进制是-3
最后,来总结一下异或特点,0异或任何数得这个数(0异或0得0),一个数与自己异或时结果为0

>>> 0 ^ 0
0
>>> 0 ^ 3
3
>>> 0 ^ -3
-3
>>> 3 ^ 3
0

按位取反 ~

首先来说,按位取反是单目运算。所以,别上来就:

>>> 2 ~ 3
  File "<stdin>", line 1
    2 ~ 3
      ^
SyntaxError: invalid syntax

显得可low了,一点都不!专!业!!!

书归正传,先把规则列出来:按位取反运算符,对数据的每个二进制位取反,即把1变为0,把0变为1
来个例子:

>>> ~ 3
-4

拿到-3的补码:

0000 0011 	# 3的补码

按每个二进制位取反:

1111 1100	# 结果是负数,还要转为原码
1000 0011	# 高位不变,其余取反
1000 0100	# 末位加一

别忘了最高位的负号,二进制的100转为十进制是-4
再来个例子:

>>> ~ -2
1

-2的补码是:

1111 1110	 # -2的补码

按位取反:

0000 0001

得到的结果一目了然,是1

按位左移 <<

先把规则列出来:左移动运算符,运算数的各二进制位全部左移若干位,而 << 右边的数字指定了移动的位数,高位丢弃,低位补0
来个示例:

>>> 2 << 3
16

先拿到2的补码:

0000 0010	# 2的补码

整体(这里也就是1)开始往左移动,移动的位数是3位,所以得的移动结果:

0001 0000

最终的十进制结果是16

按位右移 >>

先把规则列出来:右移动运算符,把>>左边的运算数的各二进制位全部右移若干位,>> 右边的数字指定了移动的位数
来个示例:

>>> 2 >> 3
0

先拿到2的补码:

0000 0010	# 2的补码

从1(从右往左数,第二位)开始往右移动,移动的位数是3位,所以得的移动结果:

0000 0000  

移动到第3位时,把1就移没了,剩下全是0最终的十进制结果是0

位运算的应用

  • 异或用来做加密混淆,比如JavaScript为了防止源码被盗,除了美化、压缩就是可以做混淆。包括C中可以做加密算法。
  • 按位与和按位或可以做矩阵、跑马灯。IOS中可以用来控制按钮的操作。
  • 可以制定不同的规则,来通过一串二进制就可以表示不同的状态信息,比如一串32位的二级制位,就可以有表现32个状态信息。

随便举几个示例

  • 示例1:来自leetcode中的一个题。题目是给定一个非空的数组,除了一个元素外,其余的元素都会出现两次,找到这个仅出现一次的元素。注意:你的算法应具有线性运行时的复杂性O(n),如果不能有额外的内存开销更好。我们的老套路是:
def f1():
    l1 = [1, 1, 2, 2, 3, 4, 4, 5, 5]
    s1 = ''.join([str(i) for i in l1])
    for i in s1:
        if s1.count(i) == 1:
            return i

我们可以用异或的特点(0异或任何数得这个数,一个数与自己异或时结果为0)来帮助我们解决这个问题:

def f2():
    l1 = [1, 2, 1, 2, 3, 4, 3, 4, 5, 5, 6, 7, 6, 8, 8]
    temp = 0
    for i in l1:
        temp ^= i
        print(i, temp)
    return temp
  • 示例2,还是leetcode中的题。给定一个整数,求出该数的二进制数中有多少个1,这里我们可以使用字符串的count来解决该问题:
print('00000000000000000000000000001011'.count('1'))
print(bin(10), bin(10).count('1'))

除此之外,我们也可以使用按位右移和按位与也可以完成该需求,我们只需要将该数不断地右移,然后和1按位与,知道这个数为0即可:

def f1(n):
    temp = 0
    for i in bin(n):  # 10的二进制是1010
        print(n & 1)
        temp += n & 1
        n = n >> 1
    return temp
print(f1(10))  # 2

see also:

Python语言中的按位运算 | 原码、反码、补码和移码 | 来自JavaScript的二进制位运算符 | LeetCode解题中位运算的运用 | leetcode | 计算机中原码,反码,补码之间的关系
欢迎斧正,that's all

posted @ 2019-03-18 14:16  听雨危楼  阅读(6954)  评论(0编辑  收藏  举报