了解位运算的运用
什么是位运算 ?
计算机在底层使用的是二进制补码进行运算。对应的二进制位进行操作,计算机只识别0和1。
位运算的好处
巧妙的使用位运算可以大量减少运行开销,优化算法。
位运算快的原因是直接跟计算机的底层二进制机器操作指令,而我们的程序代码运算最终也是由 JVM转换成计算机可执行的二进制操作指令,位运算省略了中间转换的操作,处理器直接操作所以更快。
bit 和byte
bit指“位‘,是数据传输速度的计量单位,常简写为“b”;Byte指“字节”,是文件大小的计量单位,常简写为“B”。
Byte和bit的换算关系是,1 Byte=8 bits。在电脑上,一个英文字母需要占用1 Byte的硬盘空间,一个汉字则需占用2 Byte。
例如,在我们java语言中,一个int 占4 byte,也就是占32bit。
原码
将一个数字转换成二进制(机器数)就是这个数值。
反码
反码的表示方法是:正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。
补码
补码的表示方法是:正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1。 (即在反码的基础上+1)
Java支持的7个位运算符
&:与运算符
|:或运算符
~:非运算符
^: 异或运算符
>>:右移运算符
<<:左移运算符
>>>:无符号右移运算符
位移操作:
(只针对int类型的有效,Java中,一个int的长度始终是32位,也就是4个字节,它操作的都是该整数的二进制数).也可作用于以下类型,即 byte,short,char,long(它们都是整数形式)。当为这四种类型时,JVM先把它们转换成int型再进行操作。
与(&)运算符,两个都为1时才为1,其他情况均为0。 例如:-5 & 4 =0
-5的二进制形式为:11111111 11111111 11111111 11111011
4的二进制形式为:00000000 00000000 00000000 00000100
进行逻辑运算后为:00000000 00000000 00000000 00000000
转换为十进制为:0
或(|)运算符,两个都为0时才为0,其他情况均为1。 例如:5 | 4 = -1
非(~)运算符,取反,即1变为0,0变为1。 ~(-5)=4
异或(^)运算符,相同值为0,不同值为1。 -5^4 = -1
右移(>>)运算符,m>>n,把m的二进制数右移n位,m为正数,高位全部补0,m为负数,高位全部补1。例如:5>>2 =1
左移(<<)运算符,m<<n,把m的二进制数左移n位,高位超出n位都舍弃,低位补0(此时可能出现正数变负数。 5<<2 =20
无符号右移(>>>)运算符,m>>>n,整数m表示的二进制右移n位,不论正负数,高位都补0
使用场景
-
判断奇偶数
常规做法:
if( n % 2) == 1
// n 是奇数
}
采用(&)与运算,效率直接提高。
if(n & 1 == 1){ //(n&1 == 0)//n是偶数
//n是奇数
}
原理分析:与(&)运算 两个都为1时才为1,其他情况均为0。
-
交换两个数
常规的使用是引入额外的变量来完成交换。
private void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
在面试中这个经常会做为考点,来考察内功。比如:不允许你使用额外的辅助变量来完成交换呢?
于是,用异或来完成。
private void swap(int x, int y) {
x = x ^ y // (1)
y = x ^ y // (2)
x = x ^ y // (3)
}
原理分析:异或(^)运算符,相同值为0,不同值为1
把(1)中的 x 带入 (2)中的 x,有:
y = x^y = (x^)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。
对于(3),推导如下:
x = x^y = (x^y)^x = (x^x)^y = 0^y = y。
-
找出没有重复的数
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。
这道题可能很多人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。
然而我想告诉你的是,采用位运算来做,绝对高逼格!
两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,
例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下:
由于异或支持交换律和结合律,所以:
1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5
int find(int[] arr){
int tmp = arr[0];
for(int i = 1;i < arr.length; i++){
tmp = tmp ^ arr[i];
}
return tmp;
}
时间复杂度为 O(n),空间复杂度为 O(1),而且看起来很牛逼。
-
m的n次方
如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数(Math.pow(底数,几次方)),你会怎么做呢?
思路1:
int pow(int n){
int tmp = 1;
for(int i = 1; i <= n; i++) {
tmp = tmp * m;
}
return tmp;
}
时间复杂度为 O(n)
我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:
m^1101 = m^0001 * m^0100 * m^1000。
我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:
int pow(int n){
int sum = 1;
int tmp = m;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
时间复杂度近为 O(logn),而且看起来很牛逼。
-
HashMap中哈希算法的使用
//重新计算哈希值
static final int hash(Object key) {
int h;
//key如果是null 新hashcode是0 否则 计算新的hashcode
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
^按位异或运算,只要位不同结果为1,不然结果为0;
>>> 无符号右移:右边补0,(目的是为了减少hash冲突的概率)
-
经典面试题
问题:有1000个一模一样的瓶子,其中有999瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在有一些老鼠(无穷),给你一个星期时间,问最少需要几只老鼠可以找出这瓶有毒药的水?
解答:2 ^ 10 = 1024 > 1000,所以,最少需要10个老鼠。
将1-1000表示为二进制形式:
1-> 0000 0001
2-> 0000 0010
3-> 0000 0011
4-> 0000 0100
5-> 0000 0101
6-> 0000 0110
7-> 0000 0111
8-> 0000 1000
9-> 0000 1001
10-> 0000 1010
11-> 0000 1011
12-> 0000 1100
13-> 0000 1101
……
如上所示:
1号老鼠喝第一位为1的水
2号老鼠喝第二位为1的水
3号老鼠喝第三位为1的水
4号老鼠喝第四位为1的水
5号老鼠喝第五位为1的水
……
如果i号老鼠死了,则第i位为1。否则,为0。最后得到的数字组成的二进制即为有毒水的序列。
扩展:如果你有两个星期的时间(换句话说你可以做两轮实验),为了从1000个瓶子中找出毒药,你最少需要几只老鼠?注意,在第一轮实验中死掉的老鼠,就无法继续参与第二轮实验了。
提示:用三进制数表示即可。