关于位运算的奇技淫巧

最近看了一些关于位运算的题目,受益匪浅,觉得位操作符真心强大,无懈可击!特此总结一下,感谢那些公众号(苦逼的码农_帅地)与广大博主!

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公众号名字很苦逼,可内容一点也不苦逼,逼格很高,推荐关注一波!尊重原创,再次致谢!

 

参考:

1. https://mp.weixin.qq.com/s/C6o6T9ju34vAxNBg5zobWw

 

posted @ 2019-08-14 13:14  额是无名小卒儿  阅读(942)  评论(0编辑  收藏  举报