神奇的位运算(bitwise trick)

来源:互联网 发布:mysql删除自增长 编辑:程序博客网 时间:2024/05/19 13:15

   在计算机中,数据都以二进制补码的形式存储,根据这一特点,适当采用位运算(bitwise operation)可以很巧妙地解决问题,同时运算效率更高。时刻牢记,最大的负数是-1,在计算机中,它的存储形式是全1。

位运算符

  • 左移<< 和 右移>>

   左移相当于乘以2,友谊相当于除以2.在计算机中,位运算比乘法、除法运算要快得多,所以适当采用移位运算取代乘除运算,能够提高运算效率。右移时要注意符号位的扩展。

  • 按位与&

   &运算的特点是可以将某些位置为0。
   &运算也可用于提取某些特定的位。做法是定义一个mask,mask中对应要提取的位置为1,而其它位为0,将整数与mask做&运算,就可以提取出整数中相应的位。

  • 按位或|

   |运算的特点是,可以将指定的位元置为1。

  • 按位异或^

   以下运算的规律在解决问题时可能是会经常用到的。
   x^x=0
  bit^1=~bit x^(-1)=~x
  bit^0=bit x^0=x

  • 按位取反~

   ~运算的特点是将0和1点到。以下规律值得牢记,此规律对正整数或负整数都成立。
   -x=~x+1


一些小技巧

   位运算结合在一起,恶意巧妙地解决很多问题,下面就是一些bitwise trick

  • 交换两个int
void swap(int &x, int &y){    x = x^y;    y = x^y;    //(x^y)^y = x^(y^y) = x^0 = x;    x = x^y;    //(x^y)^x = (x^x)^y = 0^y = y;}

   最早以前,我所知道的交换方法是下面这样的

void swap(int &x,int &y){    x = x+y;    y = x-y;    x = x-y;}

   这样在大多数情况下也是可以的,但是不及位运算。首先,撇开运算速度不说,通过加减交换最大的隐患在于,如果x和y的值都很大,那么在计算x+y的时候,可能发生溢出,这在位运算中是绝对不会出现的。

  • 判断一个数是不是2的幂
void PowerOf2(int x){    if (x == 0)        return false;    return x&(x-1) == 0;}

   x&(x-1)的作用是,使得x最右边的1变为0,x是2的幂,意味着x中只有一个1,所以经过这一运算后,x&(x-1)等于0。

   还有一个特点需要注意的是,如果x是2的幂,那么x-1的形式是,左边全为0,右边全为1。

   另一个解为return x&(-x) ==x;,但是具体怎么解,我还不明所以。

   x&(x-1)的作用不容小觑,我们可以用它来计算整数中1的个数,并借此来对整数做奇偶校验。

  • 计算整数中1的个数
int CountOf1(int x){    int count=0;    while(x)    {        count ++;        x = x&(x-1);    //将x最低位的1置为0    }    return count;}
  • 计算m和n中不同的位的个数
int diffmn(int m,int n){    int count=0;    int x = m^n;    //x中为1的位元代表了m和n中不同的位    while(x)    {        count ++;        x = x&(x-1);    }    return count;}
  • 整数加1和整数减1
int plus1(int x){    return -~x;    //x+1 = ~(~x)+1 = -(~x)}
int minus1(int x){    return ~-x;    //x = -(-x) = ~(-x)+1}

   注意:通过上述方法计算整数的加1或减1,比直接计算是要慢的。不过,这两个例子充分说明了位运算可以做的事情是很多的。

   老实说,这两个结果应该不是那么容易记清的吧,起码对我来说是这样的,牢记-x=~x+1,并记住大概的形式,应该就能够通过简单的变换很快得出正确的结果的吧。

  • 整数反号
void revsign(int x){    return ~x+1;    //-x = ~x+1}

void revsign(int x){    return (x^(-1))+1;    //(x^(-1))+1 = ~x+1 = -x}

   显然,第二种方法是不如第一种方法的,还是那句话,通过推导,好好体会一下位运算的神奇效果吧。

  • 判断一个数是否是奇数
bool isodd(int x){    return x&1}

   偶数的判断当然也很简单了!

  • 计算32位整数的绝对值
int abs(int x){    return (x&(x>>31))-(x>>31);}

   首先需要注意的是右移时会扩展符号位。如果x是正数,那么x>>31=0,毫无疑问,return语句中的表达式结果就是x;如果x是负数,那么x>>31的结果就是全1,也就是我们必须牢记的-1,于是

 (x^(x>>31)) - (x>>31)=(x^(-1)) - (-1)=~x + 1=-x

   所以,(x^(x>>31))-(x>>31)就是我们要求的x的绝对值。

  • 求a和b的最大值
int max(int a,int b){    int maxab = b&((a-b)>>31) | a&(~(a-b)>>31);    return maxab;}

   对于unsigned int,右移31位是一个很特别的操作,其作用是将所有的位都置为符号位的值,也就是说,正数移位后变为0,负数移位后变为-1。所以,如果a<b,那么(a-b)>>31结果就是-1,而~(a-b)>>31结果就是0,最终maxab = b;反过来,如果a>b,那么(a-b)>>31结果是0,而~(a-b)>>31结果是-1,最终maxab = a

   需要注意的是,此处假定在32位机器上进行操作,因此移位的位数是31,为了保证表达式的通用性,可以通过sizeof(int)*8 - 1来确定移位的位数。

   下面是另外一种解法,成立的条件是true=1,false=0

int max(int a,int b){    int maxab = a^((a^b)&(-(a<b)));    return maxab;}

   如果a<b,那么-(a<b)就是-1,于是maxab = a^(a^b) = b;如果a>b,那么-(a<b)就是0,于是maxab = a

   可以看到,两种解法的核心都是将a、b的关系通过-1或0来间接表示,并利用表示结果作进一步的运算。

  • 求a和b的最小值
int min(int a,int b){    int minab = a&((a-b)>>31) |b&(~(a-b)>>31);    return minab;}

   同样的,只要满足true=1,false=0,也同样可以采用第二种方法来求最小值。

int min(int a){    int minab = b^((a^b)&(-(a<b)));    return minab;}

   推导过程与注意事项同求最大值的解法是基本相同的,在此不做赘述。

  • 计算两个数的平均值
int mean(int x,int y){    return (x&y)+((x^y)>>1);}

   我们知道,计算两个数的平均值,最直接的想法就是(x+y)/2,表达式就是通过位运算实现了这一想法。表达式的核心思想是,将x+y分解成两部分,一部分是x和y相同的位,一部分是x和y不同的位,前者通过&运算完成相加(由于没有进位,所以无需除以2),后者通过^运算完成相加,然后除以2得出平均值,最后,将两部分相加,就是所求的结果了。

   同样的,通过位运算,可以成功规避相加溢出的问题。


   在此,我们瞥见了位运算的一抹影子,你当然可以在其他很多地方领略到更多神奇之处,比如在演算法笔记中。

0 0
原创粉丝点击