最近看了一些关于位运算的题目,受益匪浅,觉得位操作符真心强大,无懈可击!特此总结一下,感谢那些公众号(苦逼的码农_帅地)与广大博主!
1. 判断奇偶性
一般操作是:
1 if( n % 2) == 1{ 2 // n 是个奇数 3 }
但是!!!运用位操作符,发现,只需要判断最后一位上是否是1就行了(想想8421码的构成),只有当最后一位是1时,那么该数就是奇数,非1(也就是0)时,是偶数。如下:
1 if(n & 1 == 1){ 2 // n 是个奇数。 3 }
虽然形式基本一样,虽然我们写成 n % 2 的形式,编译器也会自动帮我们优化成位运算,但是!!!如果是你自己写出来,你自己难道不觉得自己很牛逼吗?是不是感觉逼格一下就上去了?【特此声明:别人看不懂,挨打别怪我】除此外,时间效率也快很多。
2. 交换两个数
一般操作是,借助一个中间变量:
1 int tmp = x; 2 x = y; 3 y = tmp;
但是!!!万一哪天有人抽风了要为难你,不允许你使用额外的辅助变量来完成交换呢?你还别说,有人面试确实被问过,这个时候,位运算大法就来了。代码如下:
1 x = x ^ y // (1) 2 y = x ^ y // (2) 3 x = x ^ y // (3)
运用异或运算,异或运算性质是:相同位为0,不同位为1。故自己和自己异或,肯定是0啊,如:x^x=0,【自己异自己,吃个热屁屁】,并且任何数与 0 异或等于它本身,即 n ^ 0 = n。而且这里还用到了交换律,x^y^x 和x^x^y结果是一样滴!有人可能说我还是不懂,那么上述代码,我简要分析一下,您立马就懂了:
先进行(1)中运算,x=x^y;
然后计算(2),y=x^y = (x^y)^y = (运用交换律) = x^(y^y) = (运用自己异或自己) = x^0 = x;
最后计算(3),x = x^y = (x^y)^x = (运用交换律) = (x^x)^y = y;
故:交换成功!
3. 找出没有重复的数
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。
不知您想到了啥?是不是可以利用上述第2个例子中的异或操作?
您崩急,咱先看看一般解法是啥,一般解法是利用Hash表来存储,时间复杂度是O(1),空间复杂度是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。
也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。代码如下:
1 int find(int[] arr){ 2 int tmp = arr[0]; 3 for(int i = 1;i < arr.length; i++){ 4 tmp = tmp ^ arr[i]; 5 } 6 return tmp; 7 }
时间复杂度为 O(n),空间复杂度为 O(1)。
4. m的n次方
如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数,你会怎么做呢?这还不简单,一个for循环让 n 个 m 相乘就行了,再不济整个递归。但是,这是小学生都会的东西啊,怎么对得起我的标题奇技淫巧呢?
例如:
n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为: m^n=m^1101 = m^0001 * m^0100 * m^1000 = m^1*m^4*m^8 = m^13。
那么由此带来的问题就是,如何拆解!
我们可以通过 & 1(与操作符)和 >>1(右移操作符) 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:
1 //自己重写的pow()方法 2 int pow(int n){ 3 int sum = 1; 4 int tmp = m; 5 while(n != 0){ 6 if(n & 1 == 1){ 7 sum *= tmp; 8 } 9 tmp *= tmp; 10 n = n >> 1; 11 } 12 13 return sum; 14 }
时间复杂度近为 O(logn)。
5. 找出不大于N的最大的2的幂指数
一般操作是,让 1 不断着乘以 2,在判断是否超过N:
1 int findN(int N){ 2 int sum = 1; 3 while(true){ 4 if(sum * 2 > N){ 5 return sum; 6 } 7 sum = sum * 2; 8 } 9 }
时间复杂度是 O(logn)。
在此我举例说明:
例如: n = 19,那么转换成二进制就是 00010011(这里为了方便,我采用8位的二进制来表示)。那么我们要找的数就是,把二进制中最左边的 1 保留,后面的 1 全部变为 0。即我们的目标数是 00010000。那么如何获得这个数呢?相应解法如下:
- 找到最左边的 1,然后把它右边的所有 0 变成 1,如:00010011 ==> 00011111;
- 把得到的数值加 1,可以得到 00100000,即 00011111 + 1 = 00100000;
- 把得到的 00100000 向右移动一位,即可得到 00010000,即 00100000 >> 1 = 00010000。
那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?我先给出代码再解释吧。下面这段代码就可以把最左边 1 中后面的 0 全部转化为 1:
1 n |= n >> 1; 2 n |= n >> 2; 3 n |= n >> 4;
就是通过把 n 右移,然后做或运算即可得到。
我解释下吧,我们假设最左边的 1 处于二进制位中的第 k 位(从左往右数),那么把 n 右移一位之后,那么得到的结果中第 k+1 位也必定为 1,然后把 n 与右移后的结果做或运算,那么得到的结果中第 k 和 第 k + 1 位必定是 1;同样的道理,再次把 n 右移两位,那么得到的结果中第 k+2和第 k+3 位必定是 1,然后再次做或运算,那么就能得到第 k, k+1, k+2, k+3 都是 1,如此往复下去….代码如下:
1 int findN(int n){ 2 n |= n >> 1; 3 n |= n >> 2; 4 n |= n >> 4; 5 n |= n >> 8 // 整型一般是 32 位,上面我是假设 8 位。 6 return (n + 1) >> 1; 7 }
时间复杂度近似 O(1)。
然后我和朋友讨论过后,朋友说不用全部都右移,只需要右移到左边第一个1即可,但是呢?殊不知那还要判断当前这一位1是否是最左边的1,如果说事先计算出最左边的1是第几位,那么该计算代价是否值的去做,因为就算把最左边第一个1的左侧也右移,那也是右移和或操作的代价,这是极低的。
总结:位运算很多情况下都是跟二进制扯上关系的,所以我们要判断是否是否位运算,很多情况下都会把他们拆分成二进制,然后观察特性,或者就是利用与,或,异或的特性来观察。总之,我觉得多看一些例子,加上自己多动手,就比较容易上手了。
另:以上摘自VX公众号“苦逼的码农”,我自己也做了一下修改和润饰希望变得更加通俗易懂。原vx公众号名字很苦逼,可内容一点也不苦逼,逼格很高,推荐关注一波!尊重原创,再次致谢!
参考: