位操作技巧和总结
来源:互联网 发布:手机号绑定软件查询 编辑:程序博客网 时间:2024/06/05 17:25
位操作问题先来看一个编美上面的一个题目:对于一个字节的变量,如何求得其二进制中“1”的个数,要求效率尽可能的高。二进制数有啥特征,无非是1、0;比如(10)10=(0000 1010)2 。除以2,就相当于右移一位,如果得到0,就表示那一位是0,如果得到1,就表示那一位是1.。看起来很简单的嘛,So easy啊。编码实现:
int Count(char n) { int num=0; while(n) { if(n%2==1) num++; n/=2; } return num; }
可是效率好像不是很高,对于这样简单的问题,需要提高效率。尤其是相除,计算机运算很费时间。相除好比移位,我们可以用移位来代替啊。可是把n/=2换成n>>1还不够好。对于一个二进制数,如何求得其最后一位的数字,我们可以用位操作来解决。用1与n进行与&操作,00001010&00000001=0。如果最后一位是1,相与的结果必然是1,如果最后一位是0,相与的结果必然是0。这样压根就不需要什么判断了。有时候编程会要求不能有判断语句,那时候你就会很拙计了。
编码实现:
int Count(char n) { int num=0; while(n) { num+=n&1; n>>=1; } return num; }
可是,即便是如此,算法时间复杂度依然是O(lgn)。lgn是n的二进制的位数。可以继续改进。因为二进制中1的个数一般是小于lgn的,就像64=01000000只有一个1,可是需要6次循环才能找到1的个数。可不可以只需要m次就能找到呢?m是n中1的个数。可以吗?有一个很巧妙的位操作用来查看一个数是否是2的整数幂,比如10000000=128。10000000-1=01111111=127.如果127&128,会得到什么结果?10000000&01111111=0.yes!结果为0证明就是2的整数幂。你会不会发现一个问题,最后得到的结果高位1不见了?!我们可以根据这个找到规律吗?10000110&(10000110-1)=00000110,少了一个1。00000110&(00000110-1)=00000010,有少了一个1,nice。也就是说n&(n-1)会使n中1的个数减1。很好啊,这样我们就只需m次就可以得到想要的结果了。很巧妙吧。
编码实现
int Count(char n) { int num=0; while(n) { n&=(n-1); num++; } return num; }
时间复杂度是O(m),m是n中1的个数。Very cool。还可以减少时间复杂度吗?可以O(1)吗?
哈哈,yes。我们可以利用空间换时间,把8位256个数其二进制1的个数全部放入一个数组,只需要直接返回结果就可以了。当然这需要空间很小,如果很大的话就不敢了。
还有一种很巧妙的方法,也很高效。
看个例子,N=34520,可以通过下面四步来计算其二进制中1的个数。第一步:每2位为一组,组内高低位相加 10 00 01 10 11 01 10 00 -->01 00 01 01 10 01 01 00第二步:每4位为一组,组内高低位相加 0100 0101 1001 01 00 -->0001 0010 0011 00 01第三步:每8位为一组,组内高低位相加 00010010 00110001 -->00000011 00000100第四步:每16位为一组,组内高低位相加 000000100000100 -->0000000000000111最后得到的00000000 00000111,也就是说34520二进制中1的个数是7。
可是我们如何做?方法虽好,不能实现也是白搭。
下面介绍一种非常有技巧的方法。
先分别取10000110 11011000的奇数位(从高位数)和偶数位,空位以下划线表示。 原 数 10000110 11011000 奇数位 1_0_0_1_ 1_0_1_0_ 偶数位 _0_0_1_0 _1_1_0_0然后将空出来的位置,也就是将下划线用0填充,可得 原 数 10000110 11011000 奇数位 10000010 10001000 偶数位 00000100 01010000再将奇数位右移一位,偶数位左移一位,此时将这两个数据相加即可以达到奇偶位上数据相加的效果了,very nice!! 原 数 10 00 01 10 11 01 10 00 奇数位右移 01 00 00 01 01 00 01 00 偶数位 00 00 01 00 01 01 00 00 相加得到 01 00 01 01 10 01 01 00 可以看出,结果完全达到了我们想要的效果。如何编码的实现?
OK. 取n的奇数位并将偶数位用0填充用代码实现就是n & 0xAAAA(1010101010101010) 取n的偶数位并将奇数位用0填充用代码实现就是n & 0x5555(0101010101010101) 则n =((n & 0xAAAA)>>1)+(n & 0x5555); 不过,这仅仅是第一步而已。类似的道理是一样的。第一步:n = ((n & 0xAAAA) >> 1) + (n & 0x5555);第二步:n = ((n & 0xCCCC) >> 2) + (n & 0x3333);第三步:n = ((n & 0xF0F0) >> 4) + (n & 0x0F0F);第四步:n = ((n & 0xFF00) >> 8) + (n & 0x00FF);只有四步,连循环都不用写了。时间复杂度变成了是O(lg(lgn))lgn是n的位数。此时lgn=16可是这只适用于16位的,如果是x位,那些与其相与的常数,估计都要存入数组里面。.。。。。。编码实现
const int array[8]={0xAAAA,0x5555,0xCCCC,0x3333,0xF0F0,0x0F0F,0xFF00,0x00FF}; int Count(int n){int j=0;int times=log(log(n)/log(2)+1)/log(2)+1; //需要循环的次数 //二进制1111=15,4位最大的数 times=log15=3。所以要+1cout<<"times="<<times<<endl; for(int i=0;i<times;i++) {n=((n&array[j])>>(int)pow(2,i))+(n&array[j+1]);j=j+2;}return n;}
这也就是分治思想,我们可以分批解决小问题,大问题自然而然就解决了。也可以用递归来解决。lgn=16的时候:Divide:将16位数划分为前8位和后8位,如果只有一位了,直接返回其值。Conquer:递归解决前八位和后八位的1的个数问题。Combine:返回前八位和后八位的1的个数的和。是一样的道理吧。时间复杂度也是一样O(lg(lgn))lgn是n的位数。编码实现:
int Count(int n){int num1,num2,time,n1,n2;if(1==n||0==n) //只有一位的时候直接返回 1、0return n;num1=num2=0;time=(log(n)/log(2))+1; //n的位数n1=n>>int(time/2); //取高位time/2的数n2=n&int((pow(2,time-int(time/2))-1)); //取低位time-int(time/2)的数 2^t-1cout<<"time="<<time<<" n="<<n<<endl;num1=Count(n1); //分别递归num2=Count(n2);return num1+num2;}
很nice的算法吧。。这个时候如果给你两个数A,B,求A,B二进制形式不同的位有多少?不会?开什么玩笑,只需要A异或B即可,然后用刚才的方法就可以得答案啦。。OK,这个问题就先到这里吧。
接下来看一下位操作的一些技巧。简单的可以判断奇数偶数只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((n & 1) == 0)代替if (n % 2 == 0)来判断n是不是偶数,这样计算机运算会快一些。
两个数的交换,很简单吧。我们可以用位操作来抽掉中间变量。
void Swap(int &a, int &b){if (a != b){a ^= b;b ^= a;a ^= b;}}
很简洁,可是如何理解呢。第一步 a^=b 即a=(a^b),先保存着。第二步 b^=a 即b=b^a=b^(a^b),由于^运算满足交换律,b=b^(a^b)=b^b^a。由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。第三步 a^=b 就是a=a^b,由于前面二步可知a=(a^b),b=a,所以a=a^b即a=(a^b)^a。故a会被赋上b的值。很nice的。
如何判断一个数是正数还是负数直接判断是否大于0即可。如何是一个二进制数呢,只需要看最高位即可。如何变换呢,正变负,负变正?也可以判断是否大于0.如果不要判断求出呢?或者数是以一个二进制数呢?这个时候要想一想以前学过的,取反加1.如对于-11和11 1111 0101(二进制) –取反-> 0000 1010(二进制) 加1-> 0000 1011(二进制)同样可以这样的将11变成-11 0000 1011(二进制) –取反-> 0000 0100(二进制) 加1-> 1111 0101(二进制)因此变换符号只需要取反后加1即可, ~n + 1。位操作也可以用来求绝对值,对于负数可以通过对其取反后加1来得到正数。-6: 1111 1010(二进制) –取反->0000 0101(二进制) -加1-> 0000 0110(二进制)来得到6。因此先移位来取符号位,int i = n >> 31;要注意如果a为正数,i等于0,为负数,i等于-1。然后对i进行判断——如果i等于0,直接返回。否之,返回~n+1。编码实现
int MyAbs(int n){int i = n >> 31; if(i==0)return n ; else Return ~n + 1;}
可是有时候不能判断呢,我们继续分析一下。对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反。因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值。编码实现
int MyAbs(int n){int i = n >> 31;return ((n ^ i) - i);}
很nice。
如何将一个数的高位和地位交换呢,太简单了 n = (n >> 8) | (n << 8);
我们学过字符串逆序,很简单的方法就是两边分别逆序,再总的逆序。那二进制数的逆序呢?类似循环移位。我们当然可以把二进制化为字符串来做,可是还有别的方法吗?逆序?这又是分治的思想。只要左右全部逆序,整体再来一次逆序即可。可是如何求得呢?类似刚才的讲过的高低位相加可以解决。这时候不是高低位相加了,而是高低位交换。递归可以解决,如果n=2,直接交换,相当于逆序了。递归返回,最后一次高低位互换。
下面通过4步O(lg(lgn))得到16位数据的二进制逆序:第一步:每2位为一组,组内高低位交换 10 00 01 10 11 01 10 00 -->01 00 10 01 11 10 01 00第二步:每4位为一组,组内高低位交换 0100 1001 1110 0100 -->0001 0110 1011 0001第三步:每8位为一组,组内高低位交换 00010110 10110001 -->01100001 00011011第四步:每16位为一组,组内高低位交换 0110000100011011 -->0001101101100001
和刚才的方法一样:先分别取10000110 11011000的奇数位和偶数位,空位以下划线表示。 原 数 10000110 11011000 奇数位 1_0_0_1_ 1_0_1_0_ 偶数位 _0_0_1_0 _1_1_0_0然后将空出来的位置,也就是将下划线用0填充,可得 原 数 10000110 11011000 奇数位 10000010 10001000 偶数位 00000100 01010000再将奇数位右移一位,偶数位左移一位,此时将这两个数据相加即可以达到奇偶位上数据相加的效果了,very nice!! 原 数 10 00 01 10 11 01 10 00 奇数位右移 01 00 00 01 01 00 01 00 偶数位左移 00 00 10 00 10 10 00 00 相或得到 01 00 10 01 11 10 01 00可以看出,结果完全达到了奇偶位的数据交换 取x的奇数位并将偶数位用0填充用代码实现就是x & 0xAAAA 取x的偶数位并将奇数位用0填充用代码实现就是x & 0x5555因此,第一步就用代码实现就是: x = ((x & 0xAAAA) >> 1) | ((x & 0x5555) << 1);n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1);n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2);n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4);n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8);具体代码略。
缺失的数字问题,类似机器故障问题,详见机器故障文章。很多成对出现数字保存在磁盘文件中,注意成对的数字不一定是相邻的,如2, 3, 4, 3, 4, 2……,由于意外有一个数字消失了,如何尽快的找到是哪个数字消失了?由于有一个数字消失了,那必定有一个数只出现一次而且其它数字都出现了偶数次。用搜索来做就没必要了,利用异或运算的两个特性——1.自己与自己异或结果为0,2.异或满足交换律。因此我们将这些数字全异或一遍,结果就一定是那个仅出现一个的那个数。 OK 附表一个符号
描述
运算规则
&
与
两个位都为1时,结果才为1
|
或
两个位都为0时,结果才为0
^
异或
两个位相同为0,相异为1
~
取反
0变1,1变0
<<
左移
各二进位全部左移若干位,高位丢弃,低位补0
>>
右移
各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)
参考http://blog.csdn.net/morewindows/article/details/7354571
转载请注明出处http://blog.csdn.net/sustliangbo/article/details/9289599
- 位操作技巧和总结
- 位操作基础和技巧
- [总结] C语言的位操作技巧
- 一些位运算总结和小技巧
- 位操作和一些题目总结
- 位操作技巧大全
- 位操作技巧
- 位操作技巧
- 位操作技巧
- 位操作技巧
- 位操作技巧
- 位操作小技巧
- BitHacks--位操作技巧
- C++位操作技巧
- 位操作小技巧
- 位操作技巧
- C++位操作技巧
- 位操作实现技巧
- Spring Batch Framework– introduction chapter(上)
- 增强无线路由器信号的方法与技巧
- Node.js and the JavaScript Age
- 如何判断cpu是否支持二级地址转换SLAT(EPT)
- 解析.NET 许可证编译器 (Lc.exe) 的原理与源代码剖析
- 位操作技巧和总结
- 如何设置无线路由器实现wifi连接
- 码分多址(CDMA)的本质-正交之美
- 基于ASIHTTPRequest的图片cache组件
- 【变形Floyd】HDU-1217 Arbitrage
- 路由器的基本配置及公用命令
- 解决网络不通数据只有发送不接收的问题
- linux路由内核实现分析(一)----邻居子节点(1)
- linux路由内核实现分析(一)----邻居子节点(2)