1、判断奇偶数
传统做法:
if( n % 2) == 1
// n 是个奇数
}
位运算:
如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 代表是奇数,如果是 0 代表是偶数,代码如下:
if(n & 1 == 1){
// n 是个奇数。
}
如:n=5
0 0 0 0 0 1 0 1
&
0 0 0 0 0 0 0 1
-------------------
0 0 0 0 0 0 0 1
如:n=6
0 0 0 0 0 1 1 0
&
0 0 0 0 0 0 0 1
-------------------
0 0 0 0 0 0 0 0
2、交换两个数
传统做法:
int tmp = x;
x = y;
y = tmp;
如果不允许你使用额外的辅助变量来完成交换呢?代码如下:
x = x ^ y // (1)
y = x ^ y // (2)
x = x ^ y // (3)
在此我解释下:
1. 两个相同的数异或之后结果等于 0,即 n ^ n = 0
2.任何数与 0 异或等于它本身,即 n ^ 0 = n
所以,解释如下:
把(1)中的 x 带入 (2)中的 x,有 y = x^y = (x^y)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。
对于(3),推导如下:x = x^y = (x^y)^x = (x^x)^y = 0^y = y。
这里解释一下,异或运算支持运算的交换律和结合律哦。
3、找出没有重复的数
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。
我们刚才说过,两个相同的数异或的结果是 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。
也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。所以代码如下:
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),而且看起来很牛逼。
4、m的n次方
如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数,你会怎么做呢?这还不简单,连续让 n 个 m 相乘就行了,时间复杂度为 O(n),代码如下:
int pow(int m, int n){
int tmp = 1;
for(int i = 1; i <= n; i++) {
tmp = tmp * m;
}
return tmp;
}
位运算:例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:
m^1101 = m^0001 * m^0100 * m^1000 = m^1 * m^4 * m^8
我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:
int pow(int m, int n){
int sum = 1;
int tmp = m;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
如:m 的 13 次方
n=13 第一轮后: sum=m tmp=m^2 n=6
n=6 第二轮后: sum=m tmp=m^4 n=3
n=3 第三轮后: sum=m^5 tmp=m^8 n=1
n=1 第四轮后: sum=m^13 tmp=m^16 n=0 //优化点: n=1时可直接返回sum, 不用再执行后面两个算式
最终sum=m^13
时间复杂度近为 O(logn)
5、找出不大于N的最大的2的幂指数
传统做法:就是让 1 不断着乘以 2,这样做的话,时间复杂度是 O(logn)
int findN(int N){
int sum = 1;
while(true){
if(sum * 2 > N){
return sum;
}
sum = sum * 2;
}
}
位运算:例如 N = 19,那么转换成二进制就是 0001 0011(这里为了方便,我采用8位的二进制来表示)。那么我们要找的数就是,把二进制中最左边的 1 保留,后面的 1 全部变为 0,即我们的目标数是 0001 0000。那么如何获得这个数呢?相应解法如下:
1、找到最左边的 1,然后把它右边的所有 0 变成 1
2、把得到的数值加 1,可以得到 0010 0000,即 0001 1111 + 0000 0001 = 0010 0000
3、把 得到的 0010 0000 向右移动一位,即可得到 0001 0000,即 0010 0000 >> 1 = 0001 0000
那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?下面这段代码就可以把最左边 1 中后面的 0 全部转化为 1,
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
就是通过把 n 右移并且做或运算即可得到,这种做法的时间复杂度近似 O(1),代码如下:
int findN(int n){
n |= n >> 1; //(1)
n |= n >> 2; //(2)
n |= n >> 4; //(3)
n |= n >> 8; //(4)
n |= n >> 16; //(5)
return (n + 1) >> 1;
}
说明:
(1) 执行完后,左数第 k、k+1 位一定是1, 即:左数从第k位开始,其前2位已经为1
(2) 执行完后,左数第 k+2、k+3 位一定是1,即:左数从第k位开始,其前4位已经是1
(3) ..., 即:左数从第k位开始,其前8位已经是1
(4) ..., 即:左数从第k位开始,其前16位已经是1
(5) ..., 即:左数从第k位开始,其前32位已经是1
ps:(1)~(5) 可以由 32 个 n |= n >> 1; 来替换,即每次只右移1位并作或运算,显然这样执行效率差!!
转载自:【算法技巧】位运算装逼指南【算法技巧】位运算装逼指南
位运算的歪门邪道_meet snow happy的博客-CSDN博客