Bootstrap

位运算

位运算

一、位运算概念

位运算是直接对整数在二进制中进行操作。另我们的电脑电路设计都是基于二进制的,所以在二进制层面效率很高。通常位运算多用在对程序效率要求很高的场景。以下的二进制都以8位为例。第1位是符号位,后面7位是数字位。符号位用0代表非负数,用1代表负数。

二、 机器数和真值

1、机器数

一个数在计算机中的二进制表示形式,  叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.

比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。

那么,这里的 00000011 和 10000011 就是机器数。

2、真值

因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。

例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

二、2进制编码

在探求为何机器要使用补码之前, 让我们先了解原码, 反码和补码的概念.对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.

1、原码

原码是二进制的一种表现方式。取该整数的绝对值的二进制,再加上符号位。该原码只是为了让我们看二进制更直观,直接看出正负数和比较大小。但原码不是计算机保存的二进制,所以不能直接参与计算。如下:

Original code

2、反码

反码主要是针对负数的处理。在原码的基础上,符号位不变,其他数值位取反,即把1变成0,把0变成1。反码是为了在计算机中存储二进制,但非真正的二进制值,所以也不直接参与计算。

Counter code

3、补码

补码是真正的二进制值了,主要也是针对负数。非负数不变,而负数是在反码的基础上加1。如下:

Complement

总结如下:

1、正数的二进制值,原码、补码、反码都是一样的;
2、负数的二进制值是其补码,即反码加1。

 

三. 为何要使用原码, 反码和补码(将符号位参与运算

在开始深入学习前, 我的学习建议是先"死记硬背"上面的原码, 反码和补码的表示方式以及计算方法.

现在我们知道了计算机可以有三种编码方式表示一个数. 对于正数因为三种编码方式的结果都相同:

[+1] = [00000001]原 = [00000001]反 = [00000001]补

所以不需要过多解释. 但是对于负数:

[-1] = [10000001]原 = [11111110]反 = [11111111]补

人们开始探索 将符号位参与运算, 并且只保留加法的方法. 首先来看原码:

计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的.这也就是为何计算机内部不使用原码表示一个数.

为了解决原码做减法的问题, 出现了反码:

计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在"0"这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0.

于是补码的出现, 解决了0的符号以及两个编码的问题:

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原

这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128:

(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补

-1-127的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是注意因为实际上是使用以前的-0的补码来表示-128, 所以-128并没有原码和反码表示.(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的)

使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127].

因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.

三、位运算

运算符含义功能
&按位与如果两个相应的二进制位都为1,则该位的结果值为1;否则为0。
|按位或两个相应的二进制位中只要有一个为1,该位的结果值为1。
按位异或若参加运算的两个二进制位同号则结果为0(假)异号则结果为1(真)
取反~是一个单目(元)运算符,用来对一个二进制数按位取反,即将0变1,将1变0。
<<左移左移运算符是用来将一个数的各二进制位全部左移N位,右补0。
>>右移表示将a的各二进制位右移N位,移到右端的低位被舍弃,对无符号数,高位补0。

1、按位与(&)

按位与,即按照对应位置的二进制and比较。可以把1当作true,把0当作false。1 and 1 = 1,1 and 0 = 0,0 and 1 = 0,0 and 0 = 0,如下示意图:

bitwise-and

5&1结果为1。那这个有什么用呢?举个例子:判断数字是奇数还是偶数。普通判断方法求数字除以2求余。传统做法如下:

n%2 == 0 #True为偶数,False为奇数

可以用按位与运算判断奇偶,运算效率要高很多:

n&1 == 1 #按位于运算奇数返回1,偶数返回0

2、按位或(|)

按位或,即安装对应位置的二进制or比较。1 or 1 = 1,1 or 0 = 1,0 or 1 = 1,0 or 0 = 0。如:

bitwise-or

5|3结果为7。我们可以利用这个方法对数值修正,例如取每个整数向上取奇数。若在这个基础上减1,可以得到向下取偶数。

  1. >>> map(lambda x:(x|1)-1, range(6))
  2. [0, 0, 2, 2, 4, 4]
  3. >>> map(lambda x:x|1, range(6))
  4. [1, 1, 3, 3, 5, 5]

拓展

 # 判断变量是否为0, 是0则为False,非0判断为True,
 # and中含0,返回0; 均为非0时,返回后一个值, 
2 and 0   # 返回0
2 and 1   # 返回1
1 and 2   # 返回2

# or中, 至少有一个非0时,返回第一个非0,
2 or 0   # 返回2
2 or 1   # 返回2
0 or 1   # 返回1 

3、按位异或(^)

按位异或,则是按照对应位置的二进制xor比较。1 xor 1 = 0,1 xor 0 = 1,0 xor 1 = 1,0 xor 0 = 0。例如:

bitwise-xor

5^3结果为6。出一道算法题目:已知一个数字数组。其中只有一个数字只出现1次,其他数字都出现2次。求只出现1次的数字。利用reduce函数,一句话代码搞定且效率很高。

  1. def get_one(nums):
  2. return reduce(lambda x,y: x^y, nums)
  3. nums=[1,1,3,2,4,3,4]
  4. get_one(nums)

4、按位翻转(~)

按位翻转,即不管符号位还是数值位,全部取反码。即1变成0,0变成1。翻转和按位异或有关系,翻转相当于和-1按位异或:~n = n^-1

bitwise-fanzhuan

这个也可以用于加解密运算。

5、左移运算(<<)

左移运算是将二进制数值整体向左边移动n个位置,空出来的位置补0。例如5<<2:

leftmove

左移两位,数字5变成20,多了4倍。而从二进制看多了100倍。十进制的4等于二进制的100。这么看来左移运算可以用于乘法计算。

6、右移运算(>>)

右移运算是将二进制数值整体向右边移动n个位置,空出来的位置补上符号位的数值。即正数补0,负数补1。这个右移运算可以类似上面左移运算用于乘法一样,用于除法。这个你可以自己尝试一下。右移了两位相当于101B除以100B,二进制100B对应十进制是3。可以发现该除法计算是取结果的整数部分,5/3 = 1:

rightmove

 

;