关于位运算,涉及到的知识点比较多,本文会尽可能全面的去介绍,但个人水平有限,如有错漏,烦请指正。
文章从Typora编写,一些上标下标的显示在掘金上可能显示的不好。
我认为在学习位运算前,得先了解计算机中数据的表现方式。
数据的表示
计算机中数据是以二进制流通的,那么我们就要知道将数转成二进制的方法:
十进制转二进制
短除法:将94转换成二进制:
94 / 2 余 0 得47
47 / 2 余 1 得23
23 / 2 余 1 得11
11 / 2 余 1 得5
5 / 2 余 1 得2
2 / 2 余 0 得1
最后一个粗体1也算,倒序得出:1011110
拓展:十进制转八进制
同样用短除法:将94转换成八进制:
94 / 8 余 6 得11
11 / 8 余 3 得1
同样最后一个粗体1也算,倒序得出:136
拓展:十进制转十六进制
还是短除法:将94转成16进制:
94 / 16 余14 得5
14的十六进制表示为E,虽然没有足够的数可以倒序,但还是可以得出:5E
结论:十进制转R进制,R为任何整数,通过短除法用十进制数除以R,最后倒序得出的就是R进制数。
拓展:二进制转八进制和十六进制
用上面十进制94转换后的数据来试试
二进制转八进制:1011110
8的二进制位数是000,即每三个数分一段,将1011110从左往右分段,右边最后一段不够的补0:
001 011 110
将每段转成8进制拼接起来:
1 3 6 -> 136,所以得出的八进制是136
二进制转十六进制:1011110
16的二进制位数是0000,即每四个数分一段,将1011110从左往右分段,右边最后一段不够的补0:
0101 1110
将每段转成16进制拼接起来:
5 14 -> 5E
结论:二进制转八进制、十六进制或者是其他幂指数为2的n次方的进制,将其分段并转成对应进制数,最后拼接而成的就是该进制。
R进制转十进制
R进制转十进制不能用上面的方法,有一套公式:
二进制:abc.de = a*2^2 + b*2^1 + c*2^0 + d*2^-1 + e*2^-2
七进制:abc.de = a*7^2 + b*7^1 + c*7^0 + d*7^-1 + e*7^-2
每一项用R^k^ 表示,R为进制数,二进制为2,七进制为7;k为数值位置,个位数为0,向右一位加一,向左一位减一。每一项加起来的和为10进制结果
举个例子:二进制1011110转十进制:
1*2^6 + 0*2^5 + 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 0*2^0 = 64 + 0 + 16 + 8 + 4 + 2 + 0 = 94
现在大家应该对进制转换都比较了解了,接下来就看看关于原码、反码和补码的知识。
原码、反码和补码
原码:最高位是符号位,0表示正号,1表示负号。数值0的原码有两种表现形式:[+0]原 = 0000 0000,[-0] 原 = 1000 0000。
反码:最高位是符号位,0表示正号,1表示负号。正数的反码和原码相同,负数的反码是符号位不变,其他按位取反。数值0的反码有两种表现形式:[+0]反 = 0000 0000,[-0]反 = 1111 1111。
补码:最高位是符号位,0表示正号,1表示负号。正数的补码与其原码和反码相同,负数的补码等于其反码的末位加1。数值0有唯一的编码:[+0]补 = 0000 0000,[-0]补 = 0000 0000。
上面的内容是很重要的知识点,但是暂时不需要强行看懂,后面慢慢来。
既然有了原码这样的二进制表示,我们一定会有一个疑问:
为什么会有反码和补码
原码是计算机中对数字的二进制的表现方式,但原码不能直接进行运算,可能会出错,例如:
1 + (-1) = 0000 0001原 + 1000 0001原 = 1000 0010原 = -2(10进制)
所以为了解决原码存在的这个问题,出现了反码:
1 + (-1) = 0000 0001反 + 1111 1110反 = 1111 1111反 = 1000 0000原 = -0(10进制)
虽然解决了运算问题,但是又发现了一个新的问题**-0**,在数学运算中0的正负是没有任何意义的,而且0000 0000原和1000 0000原都表示0,造成了编码位置的浪费,为了解决0的符号以及两个编码的问题,出现了补码:
1 + (-1) = 0000 0001补 + 1111 1111补 = 0000 0000补 = 0000 0000原 = 0(10进制)
这样用补码解决了-0的问题,同时还可以用1000 0000补表示-128,在范围上就比原码和补码多了一位。
- 根据运算法,减去一个正数等于加上它的负数,所以计算机可以只有加法没有减法,这样计算机的运算设计更加简单了
- 负数补码推原码等于补码符号位不变,再进行一次补码
有了以上的基础,我们可以更好了理解位运算了。
Java中的运算符
在Java中有7种运算符:
| 符号 | 含义 | 描述 |
|---|---|---|
| & | 与 | 两个比特位都为1时,结果才为1,否则为0 |
| | | 或 | 两个比特位有一个1就为1,两个都是0才为0 |
| ~ | 非 | 按位取反,1变0,0变1 |
| 异或 | 相同为0,不同为1。任何整数和自己异或的结果为0,和0异或的结果不变 | |
| << | 左移 | 将所有二进制位左移若干位,高位舍弃,低位补0,与正负无关 |
| >> | 右移 | 将所有二进制位右移若干位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1 |
| >>> | 无符号右移 | 将所有的二进制位右移若干位,低位舍弃,高位补0,与正负无关 |
先假设有两个数5和9
5的二进制是:0101
9的二进制是:1001
然后要知道一点,计算机中的二进制是补码的形式,我们在Java中执行一下代码就可以看出:
System.out.println(Integer.toBinaryString(-1));
并且因为Java中int是32位,所以结果为11111111 11111111 11111111 11111111补
与运算
与运算是两个比特位都为1时,结果才为1,否则为0,我们对5和9的两个二进制数进行运算:5 & 9
00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- &
00000000 00000000 00000000 00000001
提示:正数的原码和补码一样
所以得出:5 & 9 = 1
或运算
或运算是两个比特位有一个1就为1,两个都是0才为0:5 | 9
00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- |
00000000 00000000 00000000 00001101
同样:正数的原码和补码一样
得出:5 | 9 = 13
非运算
非运算是按位取反:~5
00000000 00000000 00000000 00000101
----------------------------------- ~
11111111 11111111 11111111 11111010 补
10000000 00000000 00000000 00000110 原
5的补码是:00000000 00000000 00000000 00000101补
取反后成了负数:11111111 11111111 11111111 11111010补
补码的补码为原码,所以对上面再进行一次补码(符号位不变,其他各位取反,末位加1):10000000 00000000 00000000 00000110原
得出:~5 = -6
异或运算
异或运算是两个比特位相同为0,不同为1:5 ^ 9
00000000 00000000 00000000 00000101
00000000 00000000 00000000 00001001
----------------------------------- ^
00000000 00000000 00000000 00001100 补
结果为:00000000 00000000 00000000 00001100补
因为正数的补码和原码一样,所以得出:5 ^ 9 = 12
左移
用正数5来做示例:5 << 2
5的补码:
00000000 00000000 00000000 00000101
二进制向左移2位,高位舍弃,低位补0:
00000000 00000000 00000000 00010100
得出:5 << 2 = 20,相当于乘以2的n次方,n为移动位数
右移
用正数9来做示例:9 >> 2,将二进制右移2位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1:
9的补码:
00000000 00000000 00000000 00001001
二进制向右移,低位舍弃,9为正数,所以高位补0
00000000 00000000 00000000 00000010
得出:9 >> 2 = 2,相当于除以2的n次方,n为移动位数
改用-9看看:-9 >> 2,将二进制右移2位,低位舍弃,高位补值与整数正负有关,正数补0,负数补1:
10000000 00000000 00000000 00001001 (-9的原码)
11111111 11111111 11111111 11110111 (-9的补码)
二进制向右移,低位舍弃,-9为负数,所以高位补1,所以:
11111111 11111111 11111111 11111101
得出的补码是负数,转成原码要再做一次补码:
10000000 00000000 00000000 00000011
得出:-9 >> 2 = -3
无符号右移
与右移运算类似,区别在于移位后不足的补0,补值与正负数无关。
运算符的数学意义
基础操作
首先先了解下位运算的一些基础操作:
a | 0 == a
a & -1 == a
a & 0 == 0
a ^ a == 0
a ^ 0 == a
a ^ -1 = |a| - 1
a |~ a == -1
a &~ a == 0
a & a == a
a | a == a
a | (a&b) == a
a & (a|b) == a
判断一个数的奇偶性
因为1的二进制是:00000000 00000000 00000000 00000001
&运算的规则是都为1时,结果为1,否则是0,所以任何数的前31位和1进行&运算都是0,如:
00000000 00000000 00000000 00000001
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx?
-----------------------------------
00000000 00000000 00000000 0000000?
只有最后一位可能有不同结果,而偶数的最后一位不可能是1,所以任意整数N是偶数的条件为:
N & 1 == 0
交换两个整数的时候不需要中间变量
int a = 1; b = 2;
a = a ^ b;
b = b ^ a;
a = a ^ b;
System.out.println(a); // 2
System.out.println(b); // 1
其实用加法运算也可以,但是要注意我们今天学的是位运算:
int a = 1, b = 2;
a = a + b;
b = a - b;
a = a - b;
System.out.println(a); // 2
System.out.println(b); // 1
变换符号,正数变负数,负数变整数
对待操作数取反再加1即可:
int a = 1;
a = ~a + 1;
System.out.println(a); // 结果为:-1
求绝对值
方法一:如果是正数直接返回,负数则通过上面变换符号的方式得到绝对值
判断是正数还是负数,可以通过右移运算,回顾一下右移运算的规则:右移若干位,低位舍弃,高位补值,如果是正数补0,负数则补1,我们用-5来举例:-5 >> 31
-5:
10000000 00000000 00000000 00000101 原
11111111 11111111 11111111 11111011 补
>> 31,即右移31位后,将本来的首位即符号位移到了最右边
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx1
前面的值,如果被操作数(-5)是正数,则会补0,负数补1
因为-5是负数,所以补1得到的值是:11111111 11111111 11111111 11111111 补,转成原码:
10000000 00000000 00000000 00000001,即值为-1
假设原值为5,则右移31位后得到的数是:00000000 00000000 00000000 00000000,值为0
所以通过判断右移31位得到的值是0还是-1,就可以知道被操作数是正数还是负数。
至于说为什么不用 if n > 0 来判断,我认为是位运算的效率更高。
看代码:
public static int abs(int n) {
int i = n >> 31; // 得到符号位,0为正数,-1为负数
return i == 0 ? n : (~n + 1); // 正数直接返回,负数则返回绝对值
}
方法二:
先上代码:
public static int abs(int n) {
return (n ^ (n >> 31)) - (n >> 31);
}
从上面的基础操作可以知道两个公式:
a ^ 0 = a
a ^ -1 = |a| - 1
所以:
当n为正数时:
(n ^ (n >> 31)) - (n >> 31)
= n ^ 0 - 0
= n
当n为负数时:
(n ^ (n >> 31)) - (n >> 31)
= n ^ -1 + 1
= |n| - 1 + 1
= |n|
判断一个数是不是2的幂
如果n是2的幂,那么n的二进制一定是首位为1,其他各位全部是0,因为:
1000(2进制) = 8(十进制) = 2的3次幂
1100(2进制) = 12(十进制),不是2的幂
1110(2进制) = 14(十进制),不是2的幂
....
先列举一些是2的幂的数:2,4,8,16,他们的二进制是:
0000 0010
0000 0100
0000 1000
0001 0000
然后是他们的减1之后的数:1,3,7,15,他们的二进制是:
0000 0001
0000 0011
0000 0111
0000 1111
当2&1、4&3、8&7、16&15的时候,结果全都是0,详细说一下4&3:
0000 0100
0000 0011
--------- &
0000 0000
其他的同理,所以可以得出是2的幂的数,会满足条件:
N & (N - 1) == 0
正数n乘以2
n << 1,如 9 << 1 = 9 * 2 = 18
同理,n乘以2的x次幂:n << x,如 9 << 2 = 9 * 2 * 2 = 36
正数n除以2
n >> 1,如 9 >> 1 = 9 / 2 = 4
同理,n除以2的x次幂:n >> x,如 9 >> 2 = 9 / 2 / 2 = 2