无协干货又来了! 位运算
相信位运算大家都不陌生,我们常采用位运算来处理二进制数。但是你真的了解位运算吗?你知道位运算有什么作用吗?你想要写出效率更高,更加底层的代码吗?本次的无线电协会技术推送,我们就来了解一下位运算的“骚操作”。
首先,让我们来回顾一下有哪些位运算符:
1.& 按位与:都为1,则结果为1,否则为0
2. | 按位或:只要有一个为1,则结果为1,否则为0
3.^ 按位异或:值不同为1,否则为0
4.~ 取反:将0变1,将1变0
5.< 6.>>X 右移:该书右移X位,移到右端的低位被舍弃,对于无符号数,高位补0
我们都知道,计算机储存数据是以二进制形式储存,而直接使用十进制数进行运算时要进行两个步骤的转换,进行一些复杂的运算时还需要更多步的转换,但是用位运算时就不用这么多步的转换,所有的运算均在二进制下完成,可以提高运算效率。
我们先来了解一下一些常用的位运算符的作用吧!
01按位异或
作用1:两变量换值
传统换值,我们需要引入第三个变量temp,那需要额外占用内存,效率低下:
temp=a;
a=b;
b=temp;
而有了按位异或,我们有了一种效率更高的换值方法
a=a^b;
b=a^b;
a=a^b;
只需重复进行三次a^b,即可不用中间变量temp完成换值,原理如下:
第一次取a^b时,求出了a和b不同的情况,并将不同的情况覆盖在a上;
第二次取a^b时,将b与 a和b不同的情况求异或,此时可以得到a的值(请仔细思考这是为何),将a的值写入b中;
第三次取a^b时,同理,将原来a的值(此时b的值就是原来a的值)与 a和b不同的情况求异或,即可得到b,将b的值写入a中;
此时,一次不用额外变量的换值就完成了。
作用2:使指定的位反转
在想要反转的位数上写1,再取异或
如:01111010,想使其后4位反转,可以将它与00001111进行∧运算,即可得到答案:01110101
0 2按位与
判断奇偶
我们可以用按位与来判断一个数的奇偶性,因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数。
如:一个数a==27,它的二进制是 0001 1011 ,当他与1取与时,a&1运算所得值为1,1==0显然不成立,所以a不是偶数,此法可以更快速的判断一个数是否是偶数。
0 3左移
左移常被用来做 * (2 ^ n)的运算,因为直接基于二进制运算,所以左移效率比 * (2 ^ n)高。
如:1<<3的值为8,由于不涉及进制转换,此法效率会更高。
0 4右移
右移常被用来做 / (2 ^ n)的运算,因为直接基于二进制运算,所以右移效率比 / (2 ^ n)高。
如:8>>3的值为1,由于不涉及进制转换,此法效率会更高。
接下来我们来看几个实际的运用场景
以下题目正确使用方法:先看题目不看答案,仔细思考解决方法,再看答案观察自己方法和位运算方法的差异。
0 1求解数组中指定数
数组中,只有一个数出现一次,剩下都出现两次,找出出现一次的数。本题的解决方法有很多,但是使用位运算应该可以说是最快,最高效,最优雅的方法。
直接使数组里的所有单元依次做异或运算,就可以得到最终的那个唯一的数。
0 2求平均数
求平均数的方法谁都会,直接(x+y)/2不就完事了
但是大家有没有想过一个问题,x+y的值有没有可能会溢出?我们知道,(x+y)/2的值肯定不会溢出(要是溢出的话一开始赋值就是错的),但是x+y的值很可能会溢出,特别是在单片机这种寸土寸金的地方,溢出更是常态,于是我们可以采取以下位运算,既能避免溢出,又能高效的进行运算:
(x & y) + ((x ^ y) >> 1)
此方法的原理有些复杂,大致是先用与运算对数值做部分平均值的提取,然后用异或并右移运算获得余下部分的平均值,因此这两部分的平均值相加后就得出了原来两数的平均值。
0 3求一个数是否是二的幂
相信大部分人使用的方法都是循环,就是让那个数反复处于2 ,看最终得到的答案是否为1,如果是1,则是2的幂。但是使用循环要进行大量的运算,能否用更高的效率得出答案呢?
让我们先来观察一下2幂的数的普遍规律:
容易发现,只要一个数是2的幂,那么它一定是最高位为1,剩下的位均为0
那么也就是说,将这个数减去1,得到的一定是形如“01111”一样的数,那么我们只需要让我们要求的数和我们要求的数减一求与,再判断是否为0即可,既:
n&(n-1)==0
若该语句为真,则是2的幂,若不为真,则不是2的幂
让我们来用一个不是2的幂的数验证一下:
11 这个数的二进制为:1011
将这个数减去1,可以得到:1010
将两个数做与运算,可以得到:1==0,显然不成立
所以在判断一个数是否为2的幂时,效率最高的方法就是执行n&(n-1)==0语句,既规避了循环,还极大的提高了效率。
0 4判断一个数是否是4的幂
同理,本题我们也可以用普通的方法:将这个数反复除以4,看最后答案是否为1
但是我们也可以仿照上题的例子,使用位运算来提高计算效率。
首先,我们按照上例,观察4的幂的规律:
我们发现,所有的是4的幂的数,首先它必须是2的幂,其次它的二级制的”1”都一定在奇数位上。所以我们可以:
先让这个数经过2的幂的检验,即执行语句:n&(n-1)==0
然后再找一个数,这个数必须满足以下条件:
它必须足够大,大到可以填满所有位
它的所有奇数位必须为1,偶数位必须为0
很容易找到这个数,就是:
01010101010101010101010101010101
(不用数了,一共有三十二位)
这样用起来比较麻烦,反正不管是16进制还是10进制都要在计算机当中涉及一次转换,那我们干脆用16进制把他表示出来:
0x55555555
然后我们将这个数和我们要求的数做一次与运算,再判断和原数是否相等,即可判断这个数是否是4的幂,即:
n*0x55555555 == n
执行完这两条语句后,即可判断出这个数是否是4的幂,即执行:
n&(n-1) == 0
n*0x55555555 == n
这里其实有一个小问题,既然n*0x55555555 == n可以直接筛选出这个数是否是4的幂,为什么还需要n&(n-1) == 0呢?
其实这个道理很简单,奇数位上是1并不是该数是4的幂的充分条件,假设我们有一个数:
101,它完全满足奇数位上为1,但是它并不是4的幂。
事实上,一个数是4的幂的充分条件应该为:该数的最高位为奇数位,且剩下的位上全部是0,所以,我们应该先判断该数是否是2的幂,将所有非最高位上有1的数全部“筛去“,此时再执行n*0x55555555 == n语句就能得到正确答案。
总结 “位运算的好处诸多,唯一的缺点应该就是可读性较差,如果能够掌握那么几种位运算的方法,应该能在实际编程中提高程序的运行效率,特别是在单片机这种“寸土寸金“的地方,使用位运算更是能锦上添花。 ”文案:李乾诚
排版:李国烜
初审:黄敏雪
复审:罗舒琪
终审:唐小煜老师