前言
在被计算机的二进制弄得劝退
之时,憋自己狠狠的补一下计算机二进制知识,就有了此文… …
一、深入理解二进制编码
1.什么是二进制编码
前面有文章提到过计算机中的单位关系,下面分析一下二进制的编码:
1字节=8位
,1字节最大的二进制数是1111 1111
。所谓二进制数就是最大只能是1,2就进位,与十进制数是一样的道理。无论什么几进制,左边是高位,右边是低位。
十进制数最大只能到9,单个数字是不能表示10的,我们所用到的10就已经是进位后的数字了。
二进制1111 1111
转化位十进制:第1位表示几个20,第2位表示几个21,第3位表示几个22,…,第n位表示几个2n-1,由此可得到1111 1111
为 1*27+1*26+1*25+1*24+1*23+1*22+1*21+1*20=255。
同理可得到计算n
位二进制数的值:1*2(n-1)+1*2(n-2)+…+1*20。
2.二进制的原码、反码和补码
2.1 原码
原码我们将十进制数字转换为二进制后的数字为原码。一个整数的绝对值大小转换为二进制数,正数不做变化,负数把二进制数的首部的0改为1表示负数。如byte类型的20
的源码为0001 0100
,共8
位二进制数表示;如byte类型的-20
的源码为1001 0100
。
反码和补码都是针对与带符号的负数而言的,带符号的正整数和无符号数的补码和反码与原码相同
2.2 反码
正数的反码与其原码相同,负数的反码就是除符号位(最高位)外其余各位取反。把二进制数中的1变为0,0变为1。如byte类型的-20的原码是1001 0100
的反码就是1110 1011
。反码的取值空间和原码相同且一一对应。
2.3 补码
正数的补码与其原码相同,负数的补码就是反码+1。一个整数,得到原码后再求其反码,再用反码加1就得到补码,常在计算负数时用到。如byte类型的-20
的原码是0001 0100
,反码1110 1011
,补码1110 1100
。
3.理解有符号位和无符号位
负数在计算机中如何表示呢,书上的东西我就不说了,头大>_<!,我就站在一个码农的角度来说一说它:
用二进制的最高位表示符号,最高位是0表示正数,最高位是1表示负数。当然我们可以指定所需要的数值为有符号还是无符号。
1byte
的无符号数值范围是0~255
,有符号的数值范围是-128~127
,后者用二进制数的第1位数表示符号
针对于有符号位的二进制数来说,上面的这种说法过于简单,容易造成混淆,比如byte类型-1
的二进制就表示为1111 1111
而不是我们所认为的1000 0001
,不急,看下表。表中红色的 1 表示负数,它是符号位(最高位);**1**后面的数是补码(补码=反码+1),而这一部分代表的值就是将补码还原为原码的值(先减1再按位取反):
二进制值(1byte) | 十进制值 |
---|---|
1000 0000 | -128 |
1000 0001 | -127 |
1000 0010 | -126 |
… | … |
1111 1111 | -1 |
它由二进制值计算为负的十进制值:以为1000 0010为例,先取数值位的补码为000 0010
,转化为反码-1,为000 0001
,再去反码为111 1110
,加上符号位为1111 1110
,就是-126。(对于-128的计算方法记住就好)。
上面的这种1111 1111
表示-1和1000 0000
表示-128的方式(符号位+补码表示)如何更好的理解?那先看看是-1大还是-128大?
当然是-1大,以此对应就知道1111 1111
大于1000 0000
了。且在计算机中,无论这个整数几字节,它全用1来表示-1(用二进制补码表示十进制数),若想获取到更小的数,就逐个减1,当数据只剩下符号位为1,其他全部为0时,就得到了此字节的最小值。
注意:1byte类型的二进制数
1111 1111
在指定无符号情况下表示255,在有符号情况下表示-1。一定要决定数据是否有符号!
4.机器数和计算机采用补码
4.1 机器数
计算机中的整数分为无符号的和带符号的。无符号的整数用来表示0和正整数;带符号的整数可以表示所有的整数。
在计算中内部,所有信息都是用二进制数串来表示,因此,正负号也必须用0,1来表示,通常我们用最高位的有效位来表示数值的符号,1表示负
,0表示正
。比如当byte类型为8bit位时,第8位即为最高有效位,它用来表示数值符号,其余的7bit位用来表示数值大小。
用最高有效位的
0/1
来表示正/负
,这种正负号数字化的机内表示形式就称为 机器数,而相应的机器外部用正负号表示的数称为 真值。将一个真值表示成二进制字串的机器数的过程称为 编码。
无符号数字和带符号的正整数没有反码和补码一说(只有原码),带符号数的负数才存在不同的编码方式(原码、反码、补码)(这一点很多人混淆,混淆以后就对这个反码、补码一头雾水了,这里需要好注意一下)。也正因为如此,我们通常认为正整数的原码、补码和反码都是一样的。带符号整数的原码、反码、补码,具体怎么求,前面已经有提到。
只有带符号的整数采用补码存储表示,浮点数另有其他存储方式
4.2 采用补码表示数值
计算机中对于有符号数值的运算都是采用补码来完成的。它的优点和特征如下。
计算机中不存在绝对的无符号表示值,而是以有符号表示的正数部分表示了无符号数值,它们在计算的时候还是会采用补码来完成
采用补码的运算的特征
- 使用补码可以将符号位和其他位同一处理,同时减法也可以按照加法来算,即使用补码来表示的数,不管是加法或减法,都直接用加法运算即可实现。
- 两个用补码表示的数相加时,如果最高位(符号位)有进位时,则进位被舍弃。
采用补码运算的优点
- 使符号位可与有效值部分一起参加运算,简化运算规则,从而简化运算器的结构,提高运算速度。
- 加法运算比减法运算更容易实现,使用加法运算代替减法运算,进一步简化计算机中运算器的线路设计。
简单分析采用补码表示数值的原因
现分别采用原码、反码和补码进行加减法。假设字长位8bit位,如使用原码、反码和补码
做减法运算:
下面的补码需要先转换为反码,反码需要先转换为原码,才能计算得到十进制数值
十进制: 1 + (-1) = 0
二进制原码: 0000 0001 + 1000 0001 = 1000 0010 -> -2(×)
二进制反码: 0000 0001 + 1111 1110 = 1111 1111 -> -0(×)//这个数不对
二进制补码: 0000 0001 + 1111 1111 = 0000 0000 -> +0(√)
十进制: 1 + (-2) = -1
二进制原码: 0000 0001 + 1000 0010 = 1000 0011 -> -3(×)
二进制反码: 0000 0001 + 1111 1101 = 1111 1110 -> -1(√)
二进制补码: 0000 0001 + 1111 1110 = 1111 1111 -> -1(√)
十进制: 2 + (-1) = 1
二进制原码: 0000 0010 + 1000 0001 = 1000 0011 -> -3(×)
二进制反码: 0000 0010 + 1111 1110 = 0000 0000 -> 原码:0111 1111(x)
二进制补码: 0000 0010 + 1111 1111 = 0000 0001 -> 1(√)
... ...
正如我们所见,使用补码计算全部正确(更多的栗子我就不举了),也仅此只有使用补码进行数值运算能正确。另外,采用补码表示数值还可防止0的机器编码有两个,+0
和-0
。
原码和补码表示0的时候会出现+0
和-0
,比如在byte类型8bit为例:
byte类型 | +0 | -0 |
---|---|---|
原码 | 0000 0000 | 1000 0000 |
补码 | 0000 0000 | 1111 1111 |
而使用补码表示的时候,只有0000 0000
表示的是0,1000 0000
表示的是-128了,并且,补码还使得数据范围比原码和补码表示的多出一位(解决了+0、-0的情况),如8bit情况下:
byte类型 | 二进制范围 | 十进制范围 |
---|---|---|
原码 | 1111 1111 ~ 0111 1111 | -127 ~ 127 |
反码 | 1000 0000 ~ 0000 0000 | -127 ~ 127 |
补码 | 1000 0000 ~ 0111 1111 | -128 ~ 127 |
5.解决补码加减运算
根据前面的分析,有十进制数转换为原码,再转换为反码,最后才到补码,之后进行计算后又再进行转换,一步又一步,甚是麻烦,这里就提出一种简便实现二进制补码计算的方法:
- 补码的加法原则:[x]补+ [y]补= [x+y]补
- 补码的减法原则:[x-y]补 =[x+(-y)]补= [x]补+ [-y]补
它是基于取余原理,推论我就不做了,感兴趣可以翻阅一下书籍。
二、二进制的位运算符
位运算符优先级很低的,低于单目运算、低于加减乘除法。对于运算符的详细分析猛戳我!
在前面对原码、反码和补码的分析中,我们知道计算机内部是以补码表示数值的,那么二进制运算符也是在补码的基础之上实现的。即所有十进制数使用二进制运算符时都是以补码计算的。
两个数之间的运算
下列示例全部为byte类型,8bit位
1.&
与
左右都为真结果为真,如1010 1100 & 1001 0110 = 1000 0100
。
&&
与&
的是有本质的区别&&
表示逻辑与,就是判断左右逻辑是否都为真,当左边为false右边就不会判断;而&
当左边为false时,还会再判断右边的。int a = 1,b = 1; if(a==0 & b++==2){}//这里b++被执行了 if(b==0 && a++==2){}//a++没有被执行 System.out.println("a="+a);//a=1 System.out.println("b="+b);//b=2
2.|
或
左右有一个为真则为真,如1010 1100 | 1001 0110 = 1011 1110
。
||
与|
的区别与&&
和&
的区别类似。||
表示逻辑或,就是判断左右逻辑是否有一个为真,当左边为true右边就不会判断;而&
当左边为true时,还会再判断右边的。
3.^
异或
两个不相同结果为真,如1010 1100 ^ 1001 0110 = 0011 1010
。。很有趣的一点是一个数字异或同一个数两次,得到它本身,如(5^10)^10 == 5
。这一点性质经常用到。
4.<<
左移
二进制位数左移,不分正负,低位补0。如1010 1101 << 1 = 0101 1010
。
5.>>
右移
二进制位数右移,正数高位补0,负数高位补1。
6.>>>
无符号右移
又称为逻辑右移,不分正负,高位补0。
一个数使用的运算
~
取反码
它的针对于一个二进制数字而使用的。0转换为1,1转换为0。如~1010 == 0101
。
三、位运算符立大功
大家都知道在算法题中,熟练使用位运算符是非常有必要的,能提高算法性能,还能简单实现需求。
🐕位运算立大功!冲冲冲!!!
1.判断数的奇偶
使用&
,判断二进制数最低位是0还是1,0就是偶数,1就是奇数
//因为位运算符优先级低于逻辑运算符,所以必须用()把a&1括起来
if((a & 1) == 0) 等价于 if(a % 2 == 0)
2.交换两个数
使用^
,不使用临时变量实现两个数的交换
a ^= b;
b ^= a;
a ^= b;
3.求某int类型的数转二进制后,1的个数
由 x & (x-1)
消去x最后一位知。循环使用x & (x-1)
消去最后一位1,计算总共消去了多少次即可。
while(x != 0){
count++;
x & (x-1);
}
上面的解法可能有点看不懂,那么这里下面这种解法就很好理解了,就是不断的先右,先记录最低位是不是1,是就+1,不是就移动:
//或
while(x != 0){
count += x & 1;
x >>>= 1;//二进制右移一位
}
4.异或性质的应用与拓展
- 数组中只有一个数出现一次,剩下都出现三次,找出出现一次的;
- 数组中只有一个数出现一次,剩下都出现两次,找出出现一次的。
- 数组中只有两个数出现一次,剩下都出现两次,找出出现一次的。
5.乘除法运算转换为位运算
根据数值的补码
左/右移
规则实现高效的乘除法
。必须是不产生数据溢出的情况,就比如byte类型的8bit位数据范围是-128~127
,就不是有byte类型的120 << 1
的情况,这它会直接转换位int类型
来计算。
乘法:a * (2^n)
等价于a << n
;
除法:a / (2^n)
等价于a >> n
。
6.取模运算转化成位运算
依赖于
&
位运算符,也必须是不产生数据溢出的情况
a % 2^n
等价于 a & (2^n-1)
,注意取余数必须是2的幂次。JDK1.8的hashmap计算hash值取余操作
那一步采用的&运算符,h & (length-1) == h % length
。
7.实现加法
下面不使用加法运算符实现加法:
public int add(int A, int B) {
while(B != 0){
int ret = A ^ B;//对应位的和
B = (A & B) << 1;//对应和的进位
A = ret;
}
return A;
}
8.判断一个数是否是2的方幂
使用 n & (n-1) == 0
判断,true就是2的方幂。
if(n>0 && ((n&(n-1))==0)){
//是
}else {
//不是
}
9.求n的因子中所有2的乘积
使用 n & (-n)
求出n的因子中所有为2的乘积。
public int factor(int n){
return n & (-n);//得到的数就是n的所有因子中2的乘积
}
/**示例:
* 1.n=10 因子组成:2*5,1*10
* n&(-n)=2
*
* 2.n=8 因子组成:2*2*2
* n&(-n)=8
*/
四、写在最后的话
关于二进制的问题也是困扰我很久的,但是奈何时间琐碎,学习和记录的二进制知识点还是很少… …,之后有机会会再查阅一些资料持续学习。
当你在代码中写入上面这些位运算符,来替换简单的乘除法或是需要很多的代码才能实现的功能时,就显得很高大上,位运算立大功🐕。