目录
1 二进制
2 原码、反码、补码
3 位运算符
4 位运算符使用技巧
上回学习运算符时,漏了位运算符,因为位运算符理解起来稍微有点复杂,所以要单独写一篇~
要理解按位运算符,要先了解计算机进行存储和计算的底层逻辑。
因此我们从最基础的二进制说起。
1 二进制
只要学过计算机,就不可能不知道二进制。
我们知道,十进制是逢十进一,譬如11,左边的1在十位上,代表10,右边的1在个位上,就是1。
把1502这个数字拆开看,就是有1个1000,5个100,0个10,2个1, ,也就是说,十进制中的位数对应的就是10的幂,个位是0次幂,十位是1次幂,百位是2次幂,以此类推……
同理,二进制中的位数对应的就是2的幂,那么对于二进制下的1010,转化成十进制下的数,就是。
用2进制数数,首先是0,然后是1,接下去是10,而不是2,因为二进制中只有0和1。
小白可以练习一下从0写到10,写完对一下结果:
2 原码、反码、补码
这三个码的产生,都和表示减法(负数)有关,他们的正数表示完全一样。
至于为什么为了表示个负数,出现了三个码,我们一个一个来说。
2.1 原码
日常生活中我们用负号(减号)解决了负数的表示问题,但在计算机中怎么加上这个负号呢?人们就想了个办法,用最高位存放符号,正数为0, 负数为1。
以4位二进值数为例,最高位是符号位,那么后面只有三位来表示数字。例如,0001表示1,要表示-1,就把最高位写成1,得到1001。
当用8位来表示一个整数时,从右往左数的第8位即为符号位,当用16位来表示一个整数时,从右往左数的第16位即为符号位。我为了少写点数字>.<,本文举例都用4位。
这种方法简单直观,但在减法运算中有问题。计算1减去1,就是0001和1001加起来,会得到1010,这是咋了?1加-1,等于-2?
因此原码无法进行减法运算。
2.2 反码
正数的反码和正数的原码完全一样,对负数原码的非符号位取反,就得到负数的反码(其实也就是将正数的反码统统取反),譬如+1的反码是0001,-1的反码就是1110。
此时我们计算+1和-1相加,即0001+1110=1111,正好是-0,相反数相加等于0,没有问题。
但此时一个0有了两种表示法,0000和1111,一个数两种表示,有点奇怪。
除此之外,虽然相反数相加没有问题,但是其他数的减法依旧不对劲,譬如0010+1110,等于10000,最高位1溢出,就是0000,所以2-1=0???
所以,这个码还是不行。
2.3 补码
正数的补码和正数的原码完全一样,负数的补码等于负数的反码+1。但是,反码加1并不是补码的真正来历,只不过补码恰好等于反码加1,这么计算更加方便而已。
关于补码的本质和定义,其实初看难以理解,但是仔细想想,会发现这就是自然而然、浑然天成的东西。
我很喜欢一个词,“十方圆满”,一直以来我的微信签名都是这四个字。那么,这个词讲的是怎样一种状态呢?
- 譬如,我们原地转个圈,就可以回到原点。
- 譬如,我们在地球上,一直往东走可以到达的地方,一直往西走,也可以到达。
- 譬如,我们把时钟的时针往前拨180度,和往后拨180度,得到的结果是一样的。
- 再譬如,爱因斯坦说,如果人能看到无限远,那么他就能看到自己的后脑勺。
我到底在说什么呢?
对于四位二进制数,最大只能存放4位,就只有0000-1111这么大的空间,就只用种排列组合的方式,空间是有限的。那么,这个空间圆不圆满呢?我们想办法让它从线性变成圆就好了,理解它,就像是理解24:00就是00:00,360°就是0°一样。
先不管负数,假设我们有一条绳子,上面从左到右依次写着0、1、2…15、16,就像这样。
我们把绳子首尾相连,也就是把写着0和16的两端拧到一起,圈成一个圆。
这个圆上只有16个数字,16就等于0,这是为了方便后面和四位二进制数的16种排列组合相对应。
我们从0出发,顺时针走1个单位,得到1,逆时针走1个单位,得到15,1+15=16。同样的,顺时针走7个单位得到7,逆时针走7个单位得到9,7+9=16。这个16有个专业的叫法,叫做模。
这里的模是什么意思呢?简单举几个例子:
- 24小时制下,24就是模。
- 转一圈360°,360就是模。
-
表上有12个刻度,12就是模。
现在看这个圆,从0开始顺时针转,数值越来越大,最后到15,再转一个单位,就又回到0,没有什么问题吧?
好,我们开始引入负数,并且把顺时针看成加法,把逆时针看成减法,这下会得到什么呢?
顺时针走1个单位,认为是加1,所以得到1,圆的右边还是1~7。逆时针走1个单位,就是减1,得到-1,同理,把圆的左边都填满,得到下图。
- 首先,距离0相同距离的数,相加等于0,这解决了相反数相加为0的问题;
- 其次,这个圆可以把减法转化成加法,a-b=c,其实等同于a+(16-b)=c,因为模是16。可以自己验证下,譬如1-2=-1(逆时针走2个单位),在这里就等价与1+14=-1(顺时针走14个单位)。
完美啊!接下来,我们把这个圆上的十进制数字,替换成二进制就好了。
问题来了,正数的二进制码毫无疑问,负数的二进制码要怎么推出来呢?还有,1对面那个问号应该是什么数字?在7和-7之间,应该是8呢,还是-8呢?
先不管那个问号是什么数,7的二进制码是0111,再加1,得到1000,问号处的二进制码应该是1000,再加1,就是10001,以此类推,我们就能补满整个圆上的码。
补完你会发现,-1本就该是1111啊!因为1111再加1,为10000,但因为四位,最高位的1溢出了,所以就得到0000,-1加1可不就是0吗!
相应的,我们可以验证-4加4,即1100+0100,等于10000,溢出位不算,0000啊!这种表示法下,所有的相反数相加都是0000~
再看看别的减法呢,譬如6-3,即0110+1101,等于10011,即0011,就是3~哈哈,都通过验证了呢!
现在就剩最后一个问题了,就是0的对面应该是什么?从7出发,加1应该是8,但是从-7出发,减1应该是-8,这个1000到底代表哪个数?
其实不难发现,圆的右边都是正数,最高位皆为0,左边都是负数,最高位皆为1,这有点像原码中人为定义的最高位是符号位,所以1000自然而然应该是-8。
虽然求补码的过程中没有特意留出一个符号位,但最终得到的补码却可以用最高位来判断正负。补码的符号位就是这么来的。
比比赖赖这么多,其实求补码没那么麻烦,可以汇总成一句话:正数补码不变,求负数补码用模减去其绝对值即可。
前面我们说过模是16,那么求a-b,其实等同于a+(16-b),所以求-b这个负数的补码,用16减b不就行了吗?比如说,求-2,就用16的二进制码减去2的二进制码。可是,四位二进制码的空间里,根本没有16这个数啊?没有就对了,因为它是模,也就是10000,在八位中16的表示是00010000。
那么,我们计算-2的补码,其实就转化成:
10000
-0010
=1110
2.4 小结
总结一下:
- 原码:将最高位作为符号位(0表示正,1表示负)。
- 反码:如果是正数,则和原码一样;如果是负数,符号位为1,其余各位取反。
- 补码:如果是正数,则和原码一样;如果是负数,将反码加上1。
很多文章在解释补码时,都是原码→反码→补码这样的思路。
先介绍最简单的原码,它方便人读数,但无法做减法,接着引申出反码,它是原码过渡补码的中间产物,但无法解决0的问题,最后引出补码,反码直接加1即可得到补码,这个码可以完美解决前面两个码的问题。但实际上我们也知道了补码的发展过程并不如此,之所以提供这样的思路,只是为了便于计算补码。
关于补码的定义和本质,我解释得挺业余的,举的例子也不够严谨,主要是为了方便自己理解和记忆。要看专业的解释,就得去找权威书籍或教材来看了。
3 位运算符
计算机底层在存储数据的时候,都是用补码存储,位运算符就是基于补码进行的计算,包括:
- 位逻辑运算符: 与&,或|,异或^,取反~。
- 位移运算符:左移<< ,右移>> 。
位运算符 | 名称 | 算法 |
---|---|---|
& | 按位与 | 两个二进制数相应位都为1,则该位的结果为1,否则为0 |
l | 按位或 | 两个二进制数相应位有一个为1时,结果位就为1 |
^ | 按位异或 | 两个二进制数相应位不同时,结果为1 |
~ | 按位取反 | 对二进制进行取反,即 1 取反为 0 ,0 取反为 1 |
<< | 按位左移 | 将二进制数左移n位,相当于乘以2的n次方 |
>> | 按位右移 | 将二进制数右移n位,相当于除以2的n次方,如果不能整除,则向下取整 |
a = 2
b = 3
print("a和b转换为二进制为:", bin(a), bin(b))
-------------------------------------
[output]: a和b转换为二进制为: 0b10 0b11
下面我就用2和3,也就是0010和0011举例。
a = 0010 #2
b = 0011 #3
a&b = 0010 #2
a|b = 0011 #3
a^b = 0001 #1
~a = 1101 #-3
a<<1 = 0100 #左移一位,相当于乘2,得到4
a>>1 = 0001 #右移一位,相当于除以2,得到1
a>>2 = 0000 #右移两位,相当于除以4,不能整除时向下取整,得到0
4 位运算符使用技巧
在日常工作中,用到位运算符的场景似乎不多,它能用来做什么呢?
4.1 按位与
通常,我们写程序判断奇偶数,是除以2看余数。现在可以用该数和1进行按位与,结果是1,就是奇数,是0,则为偶数。
def Odd_Even(x):
if x&1 == 1:
print(x,'是奇数')
else:
print(x,'是偶数')
Odd_Even(666)
---------------------
[output]: '666 是偶数'
4.2 按位或
任意数和1按位或,可以向上求最接近的奇数。
6|1
---------------------
[output]: 7
7|1
---------------------
[output]: 7
4.3 按位异或
一个数a,另一个数b进行两次异或运算,最后结果不变,即(a ^ b) ^ b = a。
5^7
---------------------
[output]: 2
5^7^7
---------------------
[output]: 5
因此用异或运算调换两个数字的值。
a = 5
b = 7
a = a^b
b = b^a
a = a^b
print(a,b)
---------------------
[output]: 7 5
当然Python中其实可以用一行代码就完成交换。
a = 5
b = 7
a,b = b,a
print(a,b)
---------------------
[output]: 7 5
简单的加密也可以用异或运算,比如实际密码是password,既怕忘了又怕直接写下来被别人看到,就可以用一个简单的key作为密钥,两者作异或运算,得到tip,把这个tip记到小本子里。忘记密码时,将key和tip做异或运算,就能得到原密码啦~
password = 587645
key = 111111
tip = password ^ key
print(tip)
---------------------
[output]: 607610
tip ^ key
---------------------
[output]: 587645
4.4 按位取反
对一个数按位取反,等于它的相反数减1。
~55
---------------------
[output]: -56
对一个数两次取反,结果不变。
~~55
---------------------
[output]: 55
4.5 按位左移
a左移b位,就是把a转为二进制后左移b位,后面缺位补0,相当于a乘以2的b次方,因为在二进制数后添一个0就相当于该数乘以2。
5<<2
---------------------
[output]: 20
4.6 按位右移
a右移b位,就是把a转为二进制后右移b位,前面缺位补0,相当于a除以2的b次方,并向下取整。
14>>2
---------------------
[output]: 3
计算机中的数是用二进制来表示的,因此位运算可以更直接、更高效地实现运算操作。对于乘2除2,二进制左右位移一下就搞定,速度非常快,所以尽量用位移来代替代码中的乘除。
最后注意一点,在Python中只能对整数进行位运算~
文中图片的水印网址为本人CSDN博客地址:BeSimple
参考链接:
1)原码、反码、补码的产生、应用以及优缺点有哪些? - 张天行的回答 - 知乎
2)原码,反码,补码的深入理解与原理
3)Python位运算用途以及用法
4)js 中位运算的应用
5)位运算简介及实用技巧(一):基础篇