培训鉴定资源库

一篇读懂python的位运算

【文章来源:】 【作者:】 【发布时间:2022-06-08】 【点击量:

什么是位运算?

简单来说,位运算是把数字转换为机器语言,也就是二进制来进行计算的一种运算形式。
在古老的微处理上,位运算比加减运算略快,要比乘除运算快的多。虽然现在随着技术的迭代,新的架构在推陈出新,位运算与加减法相差无几,但是仍然快于乘除运算。为什么这么说呢?因为位运算符直接处理每一个比特位(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

最高位为负号,二进制的101是3, 所以对应的十进制是-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:


Baidu
map