数据结构与算法--二进制详解 Python二进制算法详解 史上最详细的二进制讲解 彻底搞懂原码、反码、补码 Python的负数二进制表示形式

原码、反码、补码

机器数 和 真值

  • 机器数:
    一个数在计算机中的二进制表示形式, 叫做这个数的机器数。
    机器数是带符号的,在计算机用一个数的最高位,称为符号位:用来存放符号, 正数为0, 负数为1.
    例如:十进制中的数 +3 ,假设计算机字长为8位,转换成二进制就是:00000011 ;如果是 -3 ,就是 10000011;这里的 00000011 和 10000011 就是机器数(其实就是原码 表示形式)
  • 真值:
    因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是 “无符号数” 131(二进制数:10000011 转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值(符号位直接按照0或者1 转换成 “+”“-” 占一个位置);例如:0000 0001的真值 = +000 0001 = +1;1000 0001的真值 = –000 0001 = –1;所以 二进制表示形式:10000011 --> - 000 0011 它的真值为 -3 ;其二进制结果为 131

原码、反码、补码的基础

  • 对于一个数,计算机要使用一定的编码方式进行存储:原码,反码,补码是机器存储一个具体数字的编码方式;既然有三种存储形式,那么计算机会选取哪种通用形式呢?答案是:选取补码,即数值在计算机中是以补码的形式来存储的,下面分步来说明为什么要用补码来存?

  • 原码

(1)原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小!比如如果是8位二进制 1 :

[+1]= 0000 0001

[-1]= 1000 0001

(2)因为第一位是符号位,所以8位二进制数的取值范围就是:

[1111 1111 , 0111 1111] 
#换成 数字为:
[-127 , 127]

(3)原码的优缺点:优点是简单直观,大脑最容易理解,例如,我们用8位二进制表示一个数,+11的原码为00001011,-11的原码就是10001011;缺点就是:原码不能直接参加运算,如果运算可能会出错。例如:数学上,1+(-1)=0,而在二进制中:

00000001+10000001=10000010 # 换算成十进制为-2 ;这显然出错了

所以原码的符号位不能直接参与运算,必须和其他位分开,这就增加了硬件的开销和复杂性

  • 反码

正数的反码还是其本身负数的反码是在其原码的基础上,符号位不变,其余各个位取反

[+1] = [00000001]= [00000001][-1] = [10000001]= [11111110]'''
如果一个反码表示的是负数, 除了直观的看到它的最高位是1,它表示是个负数外,
我们无法直观的得出来它的具体数值,通常要将其转换成原码再计算
这里假如直接将负数的二进制反码,按:最高位为符号位“-” 剩下的位数按照二进制来转换的话
'''
11111110 --> -126 显然也不是原来的值 -1
  • 补码

正数的补码还是其本身负数的补码是其反码+1(也即是:在其原码的基础上, 符号位不变, 其余各位取反, 最后+1)

[+1] = [00000001]= [00000001]= [00000001][-1] = [10000001]= [11111110]= [11111111]# 如果一个补码表示的是负数, 除了直观的看到它的最高位是1,它表示是个负数外,
# 我们无法直观的得出来它的具体数值,通常要将其转换成原码再计算
# 这里假如直接将负数的二进制补码,按:最高位为符号位“-” 剩下的位数按照二进制来转换的话
11111111 --> -127 显然也不是原来的值 -1
  • 计算机为什么选用以补码的形式来存储?

(1)计算机只有加法运算,没有设置减法运算!原因是:对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单;计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂!于是人们想出了将符号位也参与运算的方法;我们知道,根据运算法则:减去一个正数等于加上一个负数,即: 1-1 = 1 + (-1) = 0;所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单!
那么计算机要选取哪种码值来做加法运算呢?我们先来一一尝试,并看结果为什么要选择补码!

# 先补充:计算机是如何计算 减法 的,看下面的例子(这里我们假设已经知道是选取补码来运算)
# 求:1-2 的值
# 首先:将1-2 变成 1+(-2),然后分别将1的补码和-2的补码拿来运算
0000 ... 0001 (正数 1补码还是本身)
+
1111 ... 1110-2 的补码)
=
1111 ... 1111 (补码)
# 上面结果一看,不对啊,怎么不是-1啊?不急,有规定,我们先看最高位符号位为1,说明它是个负数
# 如果得到的是负数的话,我们需要将它转换成原码的表示形式,如果是正数(最高位符号位为0)则不需要
# 所以我们接着做:
1111 ... 1111 (补码)
1111 ... 1110 (反码)
1000 ... 0001 (原码)
即结果为-1 

(2)如果选择 原码来 存储:

# 假设要计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]+ [10000001]= [10000010]= -2 

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

(3)如果选择 用 反码 来存储:

# 同样假设要计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]+ [1000 0001]= [0000 0001]+ [1111 1110]= [1111 1111]# 再将 反码 转成 原码:
[1111 1111]= [1000 0000]= -0 

'''
看上去可行?因为按照我们通常认为的 0 和 -0 是一样;如果 0 和 -0 是一样的话,那么就有 [0000 0000]原和 [1000 0000]原 两个编码表示0,这样显然会造成混乱,而且[1000 0000]也用来表示0造成浪费!
'''

(4)看了上面两种码值的缺点后,我们选取补码的话,就能很好的解决0的符号以及两个编码的问题:

# 同样假设要计算十进制的表达式: 1-1=0 这次我们选取补码
1-1 = 1 + (-1) = [0000 0001]+ [1000 0001]= [0000 0001]+ [1111 1111]= [0000 0000]补
(这里或许会有一个疑问:[0000 0001]+ [1111 1111]补 相加 [0000 0000]补 进位后,多出的一位 去哪了?后面会详细说明下)
# 再将补码 转成 原码:
[0000 0000]=[0000 0000]# 可以把它看成一个正数,或者是单独的0

补充:0的反码、补码 都是0
接着上面,这样0用[0000 0000]表示,而且可以用[1000 0000]表示 -128,也即是用 -128 代替了 -0 (看下面详细推导):

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

即是,相比较于反码和原码要将最高位当做一个符号位的做法,采取补码可以多表示一位(之前我们认为1111 1111是 -127 它最小也只能表示这么多了,因为最高位是符号位,剩余位数都为1了)!

同时:使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数;这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127];而使用补码表示的范围为[-128, 127];
因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位,而使用补码表示时又可以多保存一个最小值.

(5)总结:
原码+反码:8位原码和反码能够表示数的范围是 [-127–127];
补码:8位补码能够表示数的范围是 [ -128 – 127]
(在补码中用 -128代替了-0,所以补码的表示范围为:(-128-0-127)共256个)
在这里插入图片描述
(以上内容参考:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html 进行简化和修改补充)

Python中负数的处理

  • 先补充 Python中求二进制数的内置函数:bin() 以及int() 的用法
    (1)内置函数 bin() 返回一个整数 int 或者长整数 long int 的二进制表示。
>>>bin(10)
'0b1010'
>>> bin(20)
'0b10100'

(2)内置函数 int() 用于将一个字符串或数字转换为整型。
语法格式:class int(x, base=10) ;x – 字符串或数字,base – 进制数,默认十进制。

>>>int()               # 不传入参数时,得到结果0
0
>>> int(3)
3
>>> int(3.6)
3
>>> int('12',16)        # 如果是带参数base的话,12要以字符串的形式进行输入,12 为 16进制
18
>>> int('0xa',16)  
10  
>>> int('10',8)  
8

注意事项:
若 x–为纯数字时,不能设置base进制,否则报错

>>> int(3.1415926)
3

>>> int(-11.123)
-11

>>> int(2.5,10)
#报错
>>> int(2.5)
2

若 x 为 str,则 base 可略可有。
base 存在时,视 x 为 base 类型数字,并将其转换为 10 进制数字。
若 x 不符合 base 规则,则报错。

>>>int("9",2)  #报错,因为2进制无9
>>> int("9")
9 #默认10进制

>>> int("3.14",8)
>>> int("1.2")
#均报错,str须为整数
>>>int("1001",2)
9
# "1001"才是2进制格式,并转化为十进制数字9
>>> int("0xa",16)
10
# ≥16进制才会允许入参为a,b,c...
>>> int("b",8) #报错
>>> int("123",8)
83
# 视123为8进制数字,对应的10进制为83
  • Python 对于负数的存储方式和 c++/c/java 有点不一样,和上面我们说的理论有点不一样!
    先看下面的例子:
a1 = bin(-3)
print(a2)
 
a2 = bin(3)
print(a2)
 
b = bin(-3 & 0xffffffff)
print(b)
 
c = bin(0xfffffffd)
print(c)

'''输出结果分别为:
-0b11 # 特别注意这个
0b11
0b11111111111111111111111111111101
0b11111111111111111111111111111101
'''

(1)注意,此时Python的坑出现了,首先我们知道Python 中的整型也是用 补码形式存储的,Python 中 bin 一个正数(十进制表示)结果就是它的二进制补码表示形式(看上面的a2结果);但是Python 中 bin 一个负数(十进制表示),输出的是它的原码的二进制表示加上个负号,这显然不是我们要求的负数的补码,因为我们可以求出 -3的补码应该为:ob11111111111111111111111111111101,而不应该是-0b11 ,下面将详细将Python中遇到负数要如何处理!
(2)Python 中 bin 一个负数(十六进制表示),输出的是对应的二进制表示,把上面的例子拿下来↓↓↓
在这里插入图片描述

c = bin(0xfffffffd) # 看上图
print(c)
# 结果为:0b11111111111111111111111111111101 (补码)
#(上图中:4294967293不是真值,它是不考虑最高符号位,直接得出的二进制值,这个可以不看)
# 将上面结果的补码,转换成原码为:0b10000000000000000000000000000011 刚好就是-3 
# 再来用下 内置方法 int()
c = bin(0xfffffffd)
print(c)
# c='0b11111111111111111111111111111101' (补码)
c1 = '0b10000000000000000000000000000011' (c的原码)
print(int(c, base=2)) # 将c的原码直接用int算,又会出现一个坑,Python的int()也不认符号位,如下结果
'''输出结果
0b11111111111111111111111111111101
2147483651 # 这不是真值
'''
# 所以我们也不要用 int()直接去将负数的二进制数还原成10进制,下面会将快捷方法
  • 那问题又来了,怎么得到一个负数的十六进制数表示方式?
    答案是: 负数 & 0xFFFF FFFF 就可以得到这个负数的十六进制表示
    我们来推理一下:
11111111  11111111  11111111  11111101-3 的 补码)
&
11111111  11111111  11111111  111111110XFFFFFFFF=
11111111  11111111  11111111  11111101 (二进制 补码)
# 刚好可以换成 十六进制的
0xFFFFFFFD
# 所以按照上面的推导
b = bin(-3 & 0xffffffff) # 这一步相当于bin(0xfffffffd)
print(b)
# ob11111111  11111111  11111111  11111101 (-3 补码)
# 虽然绕了一圈,但是总算是解决了Python负数的正确表示形式,请看下面的总结↓↓↓
  • 总结:
    (1)Python中,所以为了:获得负数(十进制表示)的补码,需要手动将其和十六进制数 0xffffffff 进行按位与操作,得到结果也是个十六进制数,再交给 bin() 进行输出,得到的才是你想要的补码表示。
    (2)当然讲到这里,有一道Python中的算法题:求二进制中1的个数,的做法对于c++程序和python程序的区别(负数补码的区别)
def get_num(n):
    count = 0
    if n < 0:
        n = n & 0xffffffff  # 得到负数的补码表示形式
    while n:
        count += 1
        n = n & (n - 1)
    return count


print(get_num(2)) # 1 
print(get_num(-1)) # 32 

(3)但是此时不可以直接将补码扔给 int()函数来转成十进制,上面结果已经得出了,int()也会忽略符号位!那么负数的补码如何转成十进制呢?看下面详解

负数的补码如何转成十进制

(1)当然我们可以直接将补码还原成原码,然后求出十进制值,这样做需要三步,而且不适用于代码运算中,**数值在[-256,-1]**之间的负数,我们可以用下面的快捷方式来解答↓↓↓
(2)算出补码的最后8位,直接在二进制中的数 减去 256 即可!嗯?一脸懵逼,为什么这样可以?我们来举例说明:
还是以上面的 -3 上面的补码为例

 ob11111111  11111111  11111111  11111101-3 补码)
 # 我们先取它的最后8位即是 11111101
 # 将最后8位 直接忽略符号位,算出它的值为:253
 # 让后用 253-256=-3 神奇啊,Python居然可以这样

在这里插入图片描述
(3)所以我们总结下数值在[-256,-1]之间的负数,求负数补码转换成十进制的方式:

c = bin(-3 & 0xffffffff) # 模拟一个负数
# c ='0b11111111111111111111111111111101' 模拟一个负数
print(int(c[-8:], base=2) - 256) # 取最后8位,然后忽略符号位求出值再减去256
# -3 

(4)原理:结合我们上面到现在所有的知识点,我们选择了以补码的形式来存储(运算),如果直接无视符号位,8位二进制数结果就是所以位数的相加的和,它的最大值就是11111111=255,如果带上符号‘1’1111111,它的补码是‘1’0000001结果是 -1,为了能真正得到这个-1,我们必须让 255-256,所以在这个在[-256,-1]之间的负数,可以使用这个原理!

位运算符 和 移位运算符

基本概述

  • 位运算符 和 移位运算符
    在这里插入图片描述
a = 60            # 60 = 0011 1100 
b = 13            # 13 = 0000 1101 
c = 0
 
c = a & b;        # 12 = 0000 1100
print ("1 - c 的值为:", c)
 
c = a | b;        # 61 = 0011 1101 
print ("2 - c 的值为:", c)
 
c = a ^ b;        # 49 = 0011 0001
print ("3 - c 的值为:", c)
 
c = ~a;           # -61 = 1100 0011
print ("4 - c 的值为:", c)
 
c = a << 2;       # 240 = 1111 0000
print ("5 - c 的值为:", c)
 
c = a >> 2;       # 15 = 0000 1111
print ("6 - c 的值为:", c)

'''输出结果
1 - c 的值为: 12
2 - c 的值为: 61
3 - c 的值为: 49
4 - c 的值为: -61
5 - c 的值为: 240
6 - c 的值为: 15
'''

特别注意:所有的操作都是在补码的基础上来操作的,正数当然原码、反码、补码一样不用加以考虑,负数就要注意了,一定要先求出它的补码,再来进行位运算和移位运算的操作,如下例子:

# 求 ~ -5 (给 -5 取反)
# 步骤:先求出 -5的补码
1000 .... 0101 (原码) # 总共有32位,为了方便写中间就省略了
1111 .... 1010 (反码)
1111 .... 1011 (补码)
# 然后进行 取反操作(取反操作不分符号位,即对所有位都取反)
1111 .... 1011 (补码)
~
0000 .... 0100 (结果符号位为正数,所以可以直接表示为:4
# 求 -3 ^ 3 (求 -3 异或 3)
# 步骤:先求出 -3 补码
1000 .... 0011 (原码)
1111 .... 1100 (反码)
1111 .... 1101 (补码)
# 再来完成和 3 的异或
1111 .... 1101-3 的补码)
^ 
0000 .... 00113 的补码)
=
1111 .... 1110 (两者异或的结果)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
1111 .... 1110 (补码)
1111 .... 1101 (反码)
1000 .... 0010 (原码)
# 所以最后-3 ^ 3 异或的结果为 -2
  • 剖析 移位运算符

(1)<< 左移运算符
将运算对象的 各二进制位 全部左移若干位:符号位不变,低位(右边)补 0

11 << 2 = 44 
# 详细过程 
00000000 00000000 00000000 00001011 
<< 2
00000000 00000000 00000000 00101100 (因为最高位是0,它表示一个正数)
=
44
————————————
-14 <<2 =-56
# 详细过程
11111111 11111111 11111111 11110010-14的补码)
<< 2
11111111 11111111 11111111 11001000 (补码)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
11111111 11111111 11111111 11001000 (补码)
11111111 11111111 11111111 11000111 (反码)
10000000 00000000 00000000 00111000 (原码)
# 所以最后结果为:
-56

左移,对于正数来说,左移多少位等于 乘以 2 ^ (左移位数);左移1 相当于乘以2(但效率比乘法高)

(2)>> 右移运算符
将运算对象的 各二进制位 全部 右移若干位:低位溢出,符号位不变,并用符号位补溢出的高位

4 >> 2 = 1 
# 详细过程
00000000 00000000 00000000 00000100 
>> 2 
00000000 00000000 00000000 00000001(因为最高位是0,它表示一个正数)
# 所以最后结果为:
1
————————————
-14 >> 2 = -4 
# 详细过程
11111111 11111111 11111111 11110010-14的补码)
>> 2 
11111111 11111111 11111111 11111100 (补码)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
11111111 11111111 11111111 11111100 (补码)
11111111 11111111 11111111 11111011 (反码)
10000000 00000000 00000000 00000100 (原码)
# 所以最后结果为:
-4

右移,对于正数来说,右移多少位等于 除以 2 ^ (右移位数);右移 1 相当于除以2(效率比除法高哦)

妙用

  • 整形变量值互换
# 给定两个整形变量a、b要求在不使用其他变量的情况下,完成两个变量值得交换
# 当然 Python可以直接 a,b=b,a 本质是因为Python中的‘=’ 不是直接赋值,而是内存地址的引用;
# 但是其他语言 像java 要完成的话,需要借助一个中间变量 来完成交换,这里学了位运算可以妙用一下
a = 1
b = 2
a = a ^ b
b = a ^ b
a = a ^ b
print(a, b) # 2 1
# 连续 3 次 异或 操作便可以互换两变量的值
  • 最低位清零
# x&(x-1) 该操作可以把 x 二进制形式中最低位的1转化成0 例如:
x=0b1010110 
x-1=0b1010101
x&(x-1)
1010110 
&
1010101
=
1010100
  • 获取最低位的1
x = 0b01010110
x1 = x & ~(x - 1)
if x1 == 0b00000010:
    print('true')
print(bin(x1))
print(x1)
'''
true
0b10
6 
'''
  • 交换指定位置的两个比特位
def swapBit(x, i, j):
    # 如果第i位和第j位上的数值相同那就没必要进行操作
    if ((x>>i) & 1) != ((x>>j) & 1):
        x ^= ((1<<i) | (1<<j))
    return x

二进制涉及的算法

发布了146 篇原创文章 · 获赞 37 · 访问量 7845

猜你喜欢

转载自blog.csdn.net/storyfull/article/details/104015397