前言
这段时间在看左神(左程云)的算法课,其中有讲到几个关于位运算的骚操作。在此记录下来,以备后查。
1、使用异或^实现交换
排序算法中,经常需要用到交换,常见方法如下:
void swap(int[] arr,int i,int j){
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
但如果要求不使用临时变量,就地交换,咋办呢???
这里就需要位运算来表演了,代码如下:
void swap(int[] arr,int i,int j){
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
}
初看很懵逼,右边仨一样的代码,就完成交换了???
细看就明白了,把arr[i]看成A,arr[j]看成B,整个方法其实就是A ^ B ^ A=B这个规律的变体。
优点:这种写法时间空间性能都更高,且看起来更有逼格
缺点:别人不一定能看懂,且要求A和B是不同的对象,否则结果恒等于0
2、找到唯一的奇数次的数
如题,在一个数组中,只有一个数出现了奇数次,其它数都出现了偶数次,该如果找到这个数?
答:全部异或,原因是0 ^ A=A,A ^ A=0。代码如下:
int findOdd(int[] arr){
int eor=0;
if(int i=0;i<arr.length;i++){
eor^=arr[i];
}
return eor;
}
3、提取最右侧的1
如题,如何将一个整形的数,提取出其二进制编码最右侧的1。如int A=168,其二进制为00000000 00000000 00000000 10101000,提取最右侧的1的结果应该为00000000 00000000 00000000 000010000。
答:使用骚操作 A&((~A)+1)。
理解:取反的操作,使得~A的高位与A完全相反。加1的操作,使得低位那些因为取反而得的1,重新进位变成0。而最右侧的1,经过取反(变成0)和进位(变成1)这两步操作,保持不变。
int getRightMost(int a){
return a&((~a)+1);
}
好像很厉害,可是这有什么用呢?且看下一题
4、找到唯二的奇数次的数
如题,在一个数组中,只有两个不同的数出现了奇数次,其它数都出现了偶数次,该如果找到这两个数?
思路:假设A和B是要找的这两个数,那么先将数组全部异或的话,eor=A^B,因为A和B是不相同的两个数,那么eor的二进制表示中,必然存在包含1的位,这些1体现了A和B的差异(两者在这些位上只能有一个为0)。根据上一题的方法,找到eor最右侧的1,然后将整个数组,根据这个最右侧的1分成两类,将其中一类(如在该位上值为0)全部异或,即得到A。将A与eor异或,即得到B。代码如下
int[] findOdd2(int[] arr){
// eor=A^B
int eor=0;
if(int i=0;i<arr.length;i++){
eor^=arr[i];
}
//找到eor最右侧的1
int rightMost=eor&((~eor)+1);
//将arr根据最右侧的1分类,其中一类全部异或得到A
int a=0;
for(int i=0;i<arr.length;i++){
if(arr[i]&rightMost==0){
a=a^arr[i];
}
}
//将A与eor异或得到B
int b=eor^a;
int[] res={a,b};
return res;
}
评价:该题极为精巧,很能体现位运算的风骚气质
关于提取最右侧的1技巧的应用,还可以看下一题
5、求整数的二进制表示中,1的个数
思路:使用提取最右侧1的技巧,提取出一个1,累加器加1,然后抹去最右侧的1,直到该整数为0
int countBinary1(int a){
int count=0;
while(a!=0){
int rightMost=a&((~a)+1);
count++;
a=a^rightMost;
}
return count;
}
评价:相比于遍历整数的32个位,性能更高。