摘要
上一篇《C/C++刁钻问题各个击破之位运算及其应用实例(1) 》受到不少网友的关注,这对我鼓励极大,首先感谢这些网友,特别要感谢几位回复的热心网友。
上一篇中我对位运算进行了简单介绍,并谈到了如何操作整数的位,比如将某位置0、置1、翻转、查询某位是否为1等,最后在这些基本的位操作上给出了3个位运算的应用实例。本篇是上一篇的续集,我将给出6个比较复杂一点的位操作,并对比较难以理解的位操作进行图文并茂的剖析,由于不想在一篇当中写太多内容,因此本篇中的这些位操作的实际应用案例我将在以后给出,即便是这样,本篇仍然非常重要,它是我们应用位运算来解决问题的基础。
上一篇中的四个基本位操作
1. 将expr的第n(n从0开始)位设置为1: expr |= (1<<n);
2. 将expr的第n(n从0开始)位设置为0: expr &= (~(1<<n));
3. 判断expr的第n(n从0开始)位是否为1:bool b = expr &(1<<n);
4. 翻转expr的第n(n从0开始)位:expr ^= (1<<n);
本篇的8个较复杂的位操作
下面介绍本篇的重点:8个较复杂的位操作。要理解这些位操作并不容易,我将使用文字+图解的方式来描述,力求讲明白,需要说明的是,为了作图方便,我假设数据都是char类型(只有8个bit),这些位操作对其他类型的数据同样适用。
1. 将最右侧的1翻转成0:x&(x-1)
假设x=01110100,那么x,x-1和x&(x-1)的二进制表示分别如下:
x: | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
x-1: | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 |
X&(x-1): | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
可以看出,x-1的功能是:将最右侧的1翻转了,并让其后的0变成了1,而保持了其他位不变。然后x&(x-1)就使得x的最右侧的1翻转成0。
该位运算在上一篇中被用来计算二进制中有多少个1,其思路是:不停的翻转右侧的1,知道将该数翻转成0为止。
2. 向右连续传播最右侧的1位:x|(x-1)
比如说x=00101000,那么x,x-1和x|(x-1)的二进制表示分别如下:
x: | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
x-1: | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 |
X|(x-1): | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 |
从上面的过程可以看出x|(x-1)使得x中最右边的1的右边的0都被1填充了,注意这不能理解成:翻转最右侧的连续的0,而应该理解成用最右侧的1填充了它右边的所有0位。或许你要问为什么,那么你假设x =00101001,那么x|(x-1)仍然等于00101001,它并没有将最右侧的两个0翻转!
3. 检查无符号数x是否是2的整数次幂:if((x&(x-1))==0)return true;
由1可知:x&(x-1)的作用是将最右侧的1翻转为0,假如翻转之后结果变成了0,则说明原来的数x的二进制表示中只包含一个1,那么它必定是2的整数次幂。这个简单,我就不图示了。记住:2的整数次幂跟2的整数次幂减1进行与运算的结果必然为0。
4. 检查无符号数x是否等于2的整数次幂减1:if((x&(x+1))==0) return true;
2的整数次幂减1再加1之后就等于2的整数次幂,于是可以用3的方法来判断,这也非常简单,不再图示。
5. 将右侧的连续1位串翻转成0位串,其他保持不变:((x|(x-1))+1)&x
比如x等于00101100,那么
x: | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
x-1: | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
X|(x-1): | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 |
(X|(x-1))+1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
((X|(x-1))+1)&x | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
由2得知:x|(x-1) 使得x中最右边的1的右边的0都被1填充了;而(x|(x-1))+1 就使得x中那个连续的1位串和它右边的0位串都变成0,并且将连续的1位串的左边那位0变成了1,并保持其他位不变; (x|(x-1)+1)&x使得x中那个连续的1位串和它右边的所有位都置为0,而保持那个连续的1位串的左边保持不变,于是就实现了将最右边的连续的1位串翻转的功能。
文字解释绕来绕去的,非常拗口啊,没办法啊,这个关系本省就绕,希望不会把各位绕糊涂了。
6. 检查无符号整数x是否等于2的两个整数次幂之差: if((((x|(x-1))+1)&x)==0)==0) return true;
所谓2的两个整数次幂之差就是2k-2j,比如k等于6,j等于2,那么26的二进制是:01000000,22的二进制表示为00000100,26-22的二进制表示为00111100,由此可以看出2k-2j的特征是:二进制表示形式中从第j位到第k-1位都是1(从0开始数),其他bit位都是0。
由上可知:通过位运算在保持其他bit位不被改变的情况下,将这些为1的bit位全部翻转为0,如果所得的结果等于0,那么原来的数就是一定是2的两个整数次幂之差!事实上5中已经介绍了((x|(x-1))+1)&x的功能就是将x的二进制表示中右侧连续的1位串翻转为0位串,因此只需要应用3计算((X|(x-1))+1)&x的值,然后判断它是否等于0即可,为了说明这个问题,我再图示演示了这一过程,这里的x等于00111100
x: | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
x-1: | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
X|(x-1): | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
(X|(x-1))+1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
((X|(x-1))+1)&x | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
从上表看出,由于最后计算结果等于0,所以x就是两个2的整数次幂的差,其实x = 60 = 26-22。