位运算各种

最近发现位运算在很多场合有很多运用啊。大概有以下几个方面:

  • 涉及二进制及其2^n进制的运算
  • 很少字段的struct class的改写例如棋盘记录和模式比较
  • 逼格&效率比较高的小心机

几个运算符

符号 名称 含义
^ 按位亦或 同为0,异为1
$\mid $ 按位或 有1则1,双0为0
& 按位与 有0则0,双为1
~ 按位取反 变1为0,变0为1(包含符号位)
<< 按位左移(包括符号) 二进制每移一位乘二,最右补零
>> 按位右移(包括符号) 二进制每移一位除二,无符号最左补零,有符号补符号位
`>>> 按位无符号右移 对应二进制全指定的位数

以上均是按位操作,要区分逻辑运算&& ||和位运算符& |

关于取反

普通的Integer char等按位操作都是直接按照它们的二进制补码操作,int直接使用该数字的补码,char则先按照ascii转换成二进制在进行操作。
e.g.char a = 'a'a的ascii码十进制为097,二进制原码为1100001,这里注意,作为char,它在java中是一个无符号16位的整数,所以补齐它的
二进制原码 补码 反码(非负数原码 补码 反码相同)

000000000 1100001

取反后的补码

111111111 0011110

取反后的反码

111111111 0011101

取反后的源码

100000000 1100010

十进制即为 -98
同理的,byte如果存储字符型也是按照先转成ascii码进行操作,即便是字符型的数字也一样。

byte a = 2;
int b = ~a;//输出b为-3
byte c = '2';
int d = ~c;//输出d为-51 2的ascii为50(十进制) 32(十六进制)

关于亦或

亦或相当的特别同时阴森啊,有几个性质

  • a^a = 0 也就是亦或自己为0,因为每位都是相同的。
  • a^a^a = a^0 = 0^a = a
  • a^b = b^a 符合交换律
  • (a^b)^c = a^(b^c) 符合结合律
  • e = a^b^c^d <-> e^a = b^c^d <-> e^a^b= c^d <-> e^a^b^c=d

既然满足交换律结合律,则如果出现这么一种情况,

[一个数组里除了一个数值出现一次,其他都出现了两次。找出这个数值。]
将这个数组全部亦或一遍,则出现两次的都可以按照交换律结合律化为0,

数组内只存在两个奇数次出现的数,其他都出现了偶数次,找出这两个数

思路:这就是上面实例的改进,因为知道了其他数字都出现了偶数次,全体亦或的结果就是x=a^b 其中a b分别是所要找的这两个数。由于a b不同,则x必然不为零,也必然存在为1的位(可以取从右第一次出现1的位置)。同时由于是亦或操作,这个1必然是a和b相异造成的。这样,把这一位为1的分一组,这一位为0的分一组,出现偶数次的依然会被分到一组通过亦或自己消除。

    //从右寻找补码中1第一次出现的位置
    public static int getFirst1(int num){
        int index = 0;
        while(index<32){//Integer有32位
            if(((num&(1<<index))^(1<<index))==0)
                return index+1;
            else
                index++;
        }
        return -1;
    }

    //判断这个数这一位是否为1
    public static boolean is1AtPos(int num,int pos){    
        return ((num>>(pos-1))&1)==1;
    }

    //寻找这两个数
    public static int[] get2Num(int[] a){
        int [] find2Num = new int[2];
        //默认初始化0
        int rs = 0;
        for (int i : a) {
            rs ^= i;//亦或全体求x
        int pos=getFirst1(rs);//寻找第一个出现1的位
        for (int j : a) {
            if(is1AtPos(j,pos))//按这位是否位1分组
                find2Num[0] ^= j;
            else
                find2Num[1] ^= j;           
        }
        System.out.println(find2Num[0]+" "+find2Num[1]);
        return find2Num;
    }

使用位亦或^节约空间的数据交换

原理:a亦或b=c,b亦或a=c,也就是亦或满足交换律;位亦或的逆运算,也就是(a^b)^b=a等于它自己。所以,只需三个亦或运算即可交换数据。看代码。

    a ^= b;
    b ^= a;
    a ^= b;

使用亦或加密

中文配合亦或加密再加上其他的一些加密算法可以很好的掩护原文。

        char  a1='晚' ,  a2='上' ,  a3='来' ,  a4='我',  a5='家' ; 
        char secret='8' ; 
        a1=(char) (a1^secret); 
        a2=(char) (a2^secret); 
        a3=(char) (a3^secret); 
        a4=(char) (a4^secret); 
        a5=(char) (a5^secret); 
        System.out.println("密文:"+a1+a2+a3+a4+a5); 
        a1=(char) (a1^secret); 
        a2=(char) (a2^secret); 
        a3=(char) (a3^secret); 
        a4=(char) (a4^secret); 
        a5=(char) (a5^secret); 
        System.out.println("原文:"+a1+a2+a3+a4+a5);

亦或判别IP地址是否相等

使用亦或在加上按位与的方式比==加&&的方式效率高。

    static int ipv6_addr_equal(int[] in6_addr_a1, int[] in6_addr_a2)
    {
    return (((in6_addr_a1[0] ^ in6_addr_a2[0]) |
        (in6_addr_a1[1] ^ in6_addr_a2[1]) |
        (in6_addr_a1[2] ^ in6_addr_a2[2]) |
        (in6_addr_a1[3] ^ in6_addr_a2[3])) == 0);
    }

关于按位与

按位与操作一般是与1配合 判断奇偶,或者判断某位是否为1;或保留下某位位数字(与位移结合)

a&1 == 0    偶数
a&1 == 1    奇数

关于按位与

按位或操作一般与0配合

按位左右移

左右移本质上就是以bit为单元进行二进制的操作,而二进制左右移n位在十进制就意味着乘以2^n或除以2^n.按位操作可以大大提升效率
港真,很多面试都有求幂的题目。很棒,刷题狗就喜欢做这种题,然而第一次碰到这题我懵逼的采用了树。傻缺了。

将二进制数绕圈循环,取往左(右)第k个为头。很多题目里面有啊

int a = a<<k | a >> (Integer.SIZE-k); //左数第k个当头
int b = b>>k | b >> (Integer.SIZE-k); //右数第k个当头

第k位置1

    a|(1<<k);

第k位置0

    a&(~(1<<k));

取第k位的数值

    a>>k&1;

后k位为0,其余为1

    (~0)&(1<<(k+1));

后k位为1,其余为0

    ~((~0)<<k);

左右移应用-二分求幂

Caution!这个思想很重要!

思路:求a^b,如果b是一个2的整数幂,举个栗子,32,那可以进行这样的迭代,由于$32=2^5$,那么就可以这样求,

  1. 第一次求 $a_1=a^2$
  2. 第二次求 $a_2=a^4=a^{2*2}={a_1}^2$
  3. 第三次求 $a_3=a^8=a^{4*2}={a_2}^2$
  4. 第四次求 $a_4=a^{16}=a^{8*2}={a_3}^2$
  5. 第五次求 $a_5=a^{32}=a^{16*2}={a_4}^2$

如果是通过迭代幂的次数,需要32次才能达到,然而通过分治的方式,只需要5次就能解算32次幂。那如果不是2的整数幂次方呢,很好办,通过将该整数分解成各个2的整数次幂的和,例如求取a的23次方,又$23=16+4+2=1$,所以原式可化为$a^{2^4+2^2+2^1+2^0}$,通过二分的方式,变形为$$(a^{16})\cdot (a^4)\cdot (a^1)\cdot (a^0)$$.
显然!上式就是二进制和十进制转换的过程,通过4+2+1=7次计算就可解出,而非单循环的23次。
其实,解算a的16次方中,即可用到$${a^4}^{2^2}$$,即计算a^的16次方只需用到2步,总共2+2+1=5次迭代就可计算完毕。
因而解决a^b次幂的步骤是

  • 将幂次化为2进制
  • 从低位遍历,为1则乘以当前幂次,为0则计算当前幂次,然后跳过,直接移至下一位
  • 当幂指数的二进制推到最高位
    public static long Power(int a, int n){
        long ans = 1;
        System.out.println("幂指数二进制:"+Integer.toBinaryString(n));
        while (n > 0) {
            if ((n & 1)!=0) {//末位不为零
                ans *= a;
            } 
            a *= a;//计算当前幂次
            n >>= 1;//迭代每次推一位
        }
        return ans;
    }

递归版本可能更清晰

    public static int power(int a,int n)  {  
        if(n==1) return a;  
        if(n&1)  
          return power(a,n-1) * a;  
        else  
        {  
            int t=power(a,n>>1);  
            return t*t;  
        }  
    }

下面还有更炸裂

利用位移求二进制里1的奇偶

一般的思路就是,一位一位截末位,为1则亦或一下,为0则不管。对于Integer需要循环32次,对byte需要循环8次。

1数量为奇则返回1,为偶数则返回0。

    public static int get11Count(int x){
            int i=0;
            for(int j =0 ;j<32;j++){
                if((x&1)==1)
                    i ^= x&1;  
                x >>= 1;
            }
            return i;
    }

但四,这个方式没有体现位移的优越性。通过分治的思想,由于结果只求1或0,那么通过1位表达2位的信息,通过2位表达4位的信息,通过4位表达8位的信息,通过8位表达16位的信息,通过16位表达32位的信息。通过五次表达,就可以求出结果。
最关心的,其实只是最后一位

先看代码

    public static int get1Count(int x){
    //返回二进制数有偶数还是奇数个1 偶数返回0 奇数返回1
            x = x ^ (x >> 1);
            x = x ^ (x >> 2);
            x = x ^ (x >> 4);
            x = x ^ (x >> 8);
            x = x ^ (x >> 16);
        return x&1;
    }

解析一下。先拿byte举个栗子。因为byte是1个字节8个bit,所以表达-128~127即$-2^7\sim 2^7$.按照上面的方式,只需右移三次.举个例子88,二进制为$2^6+2^4+2^3=1011000$

第一次

        01011000
        00101100
    xor --------
        01110100  ->这个是结果!

结果的数值含义是,将原式中,这一位i与该位的上一位(即第i+1位)做了一次亦或,结果式中每出现一个1,则代表原式中必然邻位相异。其实,由于最终结果只需要表述奇偶,因此两个1为0,等同于抵消的含义。

第二次亦或

        01110100
        00011101
    xor --------
        01101001 —>第二次的结果

结果的含义是,每一个数字1表示##原式##中,每一位与其前3位二进制数值中1的个数为奇数;每一个数字0表示##原式##中,每一位与其前3位二进制数值中1的个数为0.

第三次亦或


        01101001
        00000110
    xor --------
        01101111 ->第三次结果

显而易见,每个1的出现表示与其前7个数字1出现奇数次。
最后取最后一位x&1,得到1,即原式中有奇数次1.结果正确。

进阶-计算二进制1的个数

最原始的方式

    int BitCount(unsigned int n)
    {
        unsigned int c =0 ; // 计数器
        while (n >0)
        {
            if((n &1) ==1) // 当前位是1
                ++c ; // 计数器加1
            n >>=1 ; // 移位
        }
        return c ;
    }

原始方式的改进

    int BitCount1(unsigned int n)
    {
        unsigned int c =0 ; // 计数器
        for (c =0; n; n >>=1) // 循环移位
            c += n &1 ; // 如果当前位是1,则计数器加1
        return c ;
    }

亦或清1法

int BitCount2(unsigned int n)
{
    unsigned int c =0 ;
    for (c =0; n; ++c)
    {
        n &= (n -1) ; // 清除最低位的1
    }
    return c ;
}

查表法1:

int BitCount3(unsigned int n) 
{ 
    // 建表
    unsigned char BitsSetTable256[256] = {0} ; 

    // 初始化表 
    for (int i =0; i <256; i++) 
    { 
        BitsSetTable256[i] = (i &1) + BitsSetTable256[i /2]; 
    } 

    unsigned int c =0 ; 

    // 查表
    unsigned char* p = (unsigned char*) &n ; 

    c = BitsSetTable256[p[0]] + 
        BitsSetTable256[p[1]] + 
        BitsSetTable256[p[2]] + 
        BitsSetTable256[p[3]]; 

    return c ; 
}

查表法2:4bit查表

int BitCount4(unsigned int n)
{
    unsigned int table[16] = 
    {
        0, 1, 1, 2, 
        1, 2, 2, 3, 
        1, 2, 2, 3, 
        2, 3, 3, 4
    } ;

    unsigned int count =0 ;
    while (n)
    {
        count += table[n &0xf] ;
        n >>=4 ;
    }
    return count ;
}

查表法3:8bit查表

int BitCount7(unsigned int n)
{ 
    unsigned int table[256] = 
    { 
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, 
    }; 

    return table[n &0xff] +
        table[(n >>8) &0xff] +
        table[(n >>16) &0xff] +
        table[(n >>24) &0xff] ;
}

###炸裂的平行算法
邻位相加,重复这一过程,直到只剩一位。

    #include <stdio.h>
    #include <iostream>

    using namespace std;

    int main(int argc, char *argv[]) {
        int x;
        while (cin >> x) {
            x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
            x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
            x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
            x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
            x = (x & 0x0000FFFF) + ((x >> 16) & 0x0000FFFF);
            cout << x << endl;
        }
        return 0;
    }

###终极大招

int BitCount5(unsigned int n) 
{
    unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);
    return ((tmp + (tmp >>3)) &030707070707) %63;
}

补充:位标志法

struct _byte 
{ 
    unsigned a:1; 
    unsigned b:1; 
    unsigned c:1; 
    unsigned d:1; 
    unsigned e:1; 
    unsigned f:1; 
    unsigned g:1; 
    unsigned h:1; 
}; 

long get_bit_count( unsigned char b ) 
{
    struct _byte *by = (struct _byte*)&b; 
    return (by->a+by->b+by->c+by->d+by->e+by->f+by->g+by->h); 
}

求二进制最高位前0的个数

int nlz(unsigned x)
{
   int n;

   if (x == 0) return(32);
   n = 1;
   if ((x >> 16) == 0) {n += 16; x <<= 16;}
   if ((x >> 24) == 0) {n += 8; x <<= 8;}
   if ((x >> 28) == 0) {n += 4; x <<= 4;}
   if ((x >> 30) == 0) {n += 2; x <<= 2;}
   n = n - (x >> 31);
   return n;
}

二进制逆序

int change(int x)
{
    x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >> 1;
    x = (x & 0x33333333) << 2 | (x & 0xCCCCCCCC) >> 2;
    x = (x & 0x0F0F0F0F) << 4 | (x & 0xF0F0F0F0) >> 4;
    x = (x & 0x00FF00FF) << 8 | (x & 0xFF00FF00) >> 8;
    x = (x & 0x0000FFFF) << 16 | (x & 0xFFFF0000) >> 16;
    return x;
}

位移求绝对值

x xor (not (x shr 31) + 1) + x shr 31
x = x ^ ((~(x>>31))+1) + x>>31;

交换二进制高16位和低16位

int change(int x)
{
   return (x >> 16) | (x << 16);
   //byte可写成(x >> 4) | (x << 4);
}

参考链接:

  1. https://www.lijinma.com/blog/2014/05/29/amazing-xor/
  2. http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html

热评文章