Python笔记

Python基础之位运算符(含原码反码补码的通俗解释)

2020-02-12  本文已影响0人  化简可得

目录

1 二进制
2 原码、反码、补码
3 位运算符
4 位运算符使用技巧

上回学习运算符时,漏了位运算符,因为位运算符理解起来稍微有点复杂,所以要单独写一篇~

要理解按位运算符,要先了解计算机进行存储和计算的底层逻辑。

因此我们从最基础的二进制说起。

1 二进制

只要学过计算机,就不可能不知道二进制。

我们知道,十进制是逢十进一,譬如11,左边的1在十位上,代表10,右边的1在个位上,就是1。

把1502这个数字拆开看,就是有1个1000,5个100,0个10,2个1, 1502=1*10^3+5*10^2+0*10^1+2*10^0,也就是说,十进制中的位数对应的就是10的幂,个位是0次幂,十位是1次幂,百位是2次幂,以此类推……

同理,二进制中的位数对应的就是2的幂,那么对于二进制下的1010,转化成十进制下的数,就是1*2^3+0*2^2+1*2^1+0*2^0=8+2=10

用2进制数数,首先是0,然后是1,接下去是10,而不是2,因为二进制中只有0和1。

小白可以练习一下从0写到10,写完对一下结果:

image

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,这么计算更加方便而已。

补码

关于补码的本质和定义,其实初看难以理解,但是仔细想想,会发现这就是自然而然、浑然天成的东西。

我很喜欢一个词,“十方圆满”,一直以来我的微信签名都是这四个字。那么,这个词讲的是怎样一种状态呢?

我到底在说什么呢?

对于四位二进制数,最大只能存放4位,就只有0000-1111这么大的空间,就只用2^4=16种排列组合的方式,空间是有限的。那么,这个空间圆不圆满呢?我们想办法让它从线性变成圆就好了,理解它,就像是理解24:00就是00:00,360°就是0°一样。

先不管负数,假设我们有一条绳子,上面从左到右依次写着0、1、2…15、16,就像这样。


image

我们把绳子首尾相连,也就是把写着0和16的两端拧到一起,圈成一个圆。

image

这个圆上只有16个数字,16就等于0,这是为了方便后面和四位二进制数的16种排列组合相对应。

我们从0出发,顺时针走1个单位,得到1,逆时针走1个单位,得到15,1+15=16。同样的,顺时针走7个单位得到7,逆时针走7个单位得到9,7+9=16。这个16有个专业的叫法,叫做

这里的模是什么意思呢?简单举几个例子:

现在看这个圆,从0开始顺时针转,数值越来越大,最后到15,再转一个单位,就又回到0,没有什么问题吧?

好,我们开始引入负数,并且把顺时针看成加法,把逆时针看成减法,这下会得到什么呢?

顺时针走1个单位,认为是加1,所以得到1,圆的右边还是1~7。逆时针走1个单位,就是减1,得到-1,同理,把圆的左边都填满,得到下图。

image

完美啊!接下来,我们把这个圆上的十进制数字,替换成二进制就好了。

image

问题来了,正数的二进制码毫无疑问,负数的二进制码要怎么推出来呢?还有,1对面那个问号应该是什么数字?在7和-7之间,应该是8呢,还是-8呢?

先不管那个问号是什么数,7的二进制码是0111,再加1,得到1000,问号处的二进制码应该是1000,再加1,就是10001,以此类推,我们就能补满整个圆上的码。

image

补完你会发现,-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。

虽然求补码的过程中没有特意留出一个符号位,但最终得到的补码却可以用最高位来判断正负。补码的符号位就是这么来的。

image

比比赖赖这么多,其实求补码没那么麻烦,可以汇总成一句话:正数补码不变,求负数补码用模减去其绝对值即可。

前面我们说过模是16,那么求a-b,其实等同于a+(16-b),所以求-b这个负数的补码,用16减b不就行了吗?比如说,求-2,就用16的二进制码减去2的二进制码。可是,四位二进制码的空间里,根本没有16这个数啊?没有就对了,因为它是模,也就是10000,在八位中16的表示是00010000。

那么,我们计算-2的补码,其实就转化成:
10000
-0010
=1110

2.4 小结

总结一下:

image

很多文章在解释补码时,都是原码→反码→补码这样的思路。

先介绍最简单的原码,它方便人读数,但无法做减法,接着引申出反码,它是原码过渡补码的中间产物,但无法解决0的问题,最后引出补码,反码直接加1即可得到补码,这个码可以完美解决前面两个码的问题。但实际上我们也知道了补码的发展过程并不如此,之所以提供这样的思路,只是为了便于计算补码。

关于补码的定义和本质,我解释得挺业余的,举的例子也不够严谨,主要是为了方便自己理解和记忆。要看专业的解释,就得去找权威书籍或教材来看了。

3 位运算符

计算机底层在存储数据的时候,都是用补码存储,位运算符就是基于补码进行的计算,包括:

  1. 位逻辑运算符: 与&,或|,异或^,取反~。
  2. 位移运算符:左移<< ,右移>> 。
位运算符 名称 算法
& 按位与 两个二进制数相应位都为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)位运算简介及实用技巧(一):基础篇

上一篇 下一篇

猜你喜欢

热点阅读