位操作技巧和总结

来源:互联网 发布:手机号绑定软件查询 编辑:程序博客网 时间: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      奇数位 100000110001000      偶数位 00000100 01010000再将奇数位右移一位,偶数位左移一位,此时将这两个数据相加即可以达到奇偶位上数据相加的效果了,very nice!!      原 数      10 00 01 10 11 01 10 00      奇数位右移 000000000      偶数位     00000 00000      相加得到   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位为一组,组内高低位交换       10010  10100   -->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      奇数位 100000110001000      偶数位 00000100 01010000再将奇数位右移一位,偶数位左移一位,此时将这两个数据相加即可以达到奇偶位上数据相加的效果了,very nice!!      原 数      10 00 01 10 11 01 10 00      奇数位右移 000000000      偶数位左移 001011000      相或得到   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