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