Java位运算的一些骚操作,你知道几个?

今天来总结一下Java中的位运算,并介绍几个位运算的骚操作,请坐稳扶好,下面发车😄

1. 位运算基本介绍

1.1. 位运算适用的数据类型

  • 在Java中,位运算适用于基础数据类型中的整数类型,即byte,short,char,int,long ,引用类型,布尔类型,浮点类型不可以使用。
  • byte,short,char类型在使用位运算的时候会被转换为int类型(符号位扩展),其结果也是int类型(表达式中没有long);
  • 含有long类型的位运算的表达式的结果是long类型

我这里就不一一验证了哈。

1.2. 位运算介绍

在说位运算之前,我们先看一下逻辑运算,逻辑运算是离散数学中的概念,我们在高中的时候应该也学过,或,且,非等运算,举两个例子:

  • 逻辑与,a且b,它的结果是,一假则假,a,b只要有一个是假,结果就是假的,否则是真
  • 逻辑或,a或b,它的结果是,一真则真,a,b只要有一个是真,结果就是真的,否则是假

位运算跟逻辑运算是相通的,把每一位的0看作假,1看作真,在这样的基础上进行的逻辑运算。因此,

  • 按位与(&),如果两位都是1,结果是1,否则是0;
  • 按位或(|),如果两位都是0,结果是0,否则是1;
  • 按位取反(~),如果某一位是1,结果是0,否则结果是1;
  • 按位异或(^),如果两位代表的数值一样,则结果是0,否则是1。
  • 左移操作,a<<m,表示把整数a,左移m位,m可以是任何整数类型,可以是0,负整数,正整数
    • 当m等于0时,相当于什么也没做
    • 当a是int类型时,(byte,char,short会自动扩展成int)
      • m大于0,且m小于32,相当于把a按位左移m位,低位补0,高位舍去。
      • m大于等于32,int类型4字节,32位,左移32位,相当于啥也没干,因此当m大于32时的结果,相当于a<<(m%32)的结果;
      • m小于0,且m大于-32,例如m=-1,左移-1位,相当于左移32位时,上一次的结果,即左移31位的结果,此时结果相当于a<<(32+m)的结果
      • m小于等于-32,int类型4字节,32位,左移-32位,相当于啥也没干,因此当m小于-32时的结果,相当于a<<(32-(-m)%32)的结果
    • 当a时long类型时,只需把结论中的32换成64即可,不再赘述
  • 右移操作,a>>m,相当于把a按位右移m位,低位舍去,高位补符号位(啥是符号位?)。剩余的规则跟左移是一样的。
  • 无符号右移,可以理解为普通的右移,只不过高位补0

1.3. 对异或的理解

异或规则是,如果两位代表的数值一样,则结果是0,否则是1。可以理解为什么呢?可以理解为,如果两个位中1的个数是偶数,则结果为0,否则结果为1。
在这里插入图片描述
既然,异或可以这么理解,那么多个数同时异或的时候,异或的结果假设为ans,即
a1 ^ a2 ^ a3 ^.....an = ans
那ans的每一位,都是所有的数在这一位上的1的个数的奇偶性决定的,如果所有的数在这一位上1的个数加起来是奇数,那ans的这一位就是1,否则是0。
而结果中1的个数的奇偶性,跟异或的顺序是没有关系的,不可能说,a1和a3先异或,就会导致结果不一样,因此异或操作满足交换律和结合律。

有几个显而易见的小结论:

  1. 当一个数和自己异或的时候,a^a = 0,
  2. 一个数和0异或的时候,a^0 = a;
  3. 当一个数和-1异或的时候,a^-1 = ~a,

那异或还可以理解为什么呢?
可以理解为无进位相加,a^b相当于a和b按位无进位相加,无进位相加,就是按照加法的规则,一位一位的相加,只不过,不考虑进位;
在这里插入图片描述
那a+b的结果相当于a和b无进位相加的结果,加上进位信息,进位信息怎么算?
啥时候产生进位??,只有两位都是1的时候会产生,因此a和b的进位信息就是(a&b)<<1,为啥左移一位?因为进位是向上进位的。因此可以使用位运算实现加法。代码如下:

    public int add(int a, int b) {
        int t;
        while (b != 0) {
            t = a;//t 先抓住上一次的a
            a ^= b;// a保留a和b无进位相加的结果
            b = (b & t) << 1;//b和原来的a相与,保留进位信息
        }
        return a;
    }

2. 位运算应用

2.1. 快速乘法

a*b,当b是2的n次幂时,即 b = 2 n b =2^n b=2n,a*b = a<<n;

2.2. 快速除法

a b \frac{a}{b} ba,当b是2的n次幂时,即 b = 2 n b =2^n b=2n a b \frac{a}{b} ba = a>>n;

2.3. 快速取模

a%b,当b是2的n次幂时,即 b = 2 n b =2^n b=2n,a%b = a&(b-1);为啥,因为 b = 2 n b =2^n b=2n,因此b的二进制中,第 n n n位为1,而 b − 1 = 1 − 2 n 1 − 2 = 2 0 + 2 1 + 2 2 + . . . + 2 n − 1 b-1 =\frac{1-2^n}{1-2}=2^0+2^1+2^2+...+2^{n-1} b1=1212n=20+21+22+...+2n1(等比数列求和),相当于把第n位置0,后面的所有位都置1。
在这里插入图片描述
而a%b不就是,a中去掉几个x个b,剩下不够一个b的部分吗?如果a<b,那么a<=b-1,a和全都是1的部分相与,那结果显然正确;如果a>b,那a至少第n位是1,有可能第n+1位,第n+2位是1,但是这些位代表的数,都是b的整倍数,剩下不足一个b的部分,就是a的低n-1位组成的数,才是余数,所以取a的低n-1位就是结果。
在这里插入图片描述

2.4. 对称加密

对于两个整数a,b,如果 a = a^b,此时a成了a和b异或的结果,a此时再次异或b,a = a^b^b = a^0 = a,又变回了a,此时b相当于一个对称密钥的作用。

2.5. 两数交换

不使用额外变量交换两个数,直接上代码:

    public void swap(int a, int b) {
        a ^= b; //此时 a的值是 a^b
        b ^= a; // b = (a^b) ^b  = a;
        a ^= b; // a = (a^b) ^a(b已经变成a了)  = b;
    }

如果交换的是数组元素,代码如下:

    public void swap(int[] arr, int i, int j) {
        if (i != j) {
            arr[i] ^= arr[j];
            arr[j] ^= arr[i];
            arr[i] ^= arr[j];
        }
    }

异或的两个元素不能同时指向同一块内存,否则这个内存的数变成0

2.6. 位图(bitmap)

假设需要实现一个集合,可以往集合中放入自然数,然后判断一个数是不是存在于集合中,我们可以使用hash表实现,当数的范围比较集中的时候,我们可以使用一个bit位是0或1表示这个数是否在集合中。代码如下:

public class BitMap {

    int[] data;
    int max;

    /**
     * @param max bitmap能保存的最大自然数
     */
    public BitMap(int max) {
        if (max < 0) throw new IllegalArgumentException("max number can not less than zero");
        this.max = max;
        data = new int[(max + 31) >> 5];
    }

    public void put(int number) {
        if (number > max) throw new IllegalArgumentException("number can not greater than max");
        // 先找到这个数应该存在哪个整数里,粗略定位
        // 再确定这个数在哪一位上
        //然后把这一位置1
        data[number >> 5] |= (1 << (number & 31));
    }

    public void remove(int number) {
        if (number > max) throw new IllegalArgumentException("number can not greater than max");
        data[number >> 5] &= ~(1 << (number & 31));
    }

    public boolean contains(int number) {
        if (number > max) throw new IllegalArgumentException("number can not greater than max");
        return (data[number >> 5] & (1 << (number & 31))) != 0;
    }
}

2.7. 确定一个int整数中有几个位是1

解法1: 依次测试每一位是0 还是1,然后设置一个计数器,当前位是1,则计数器加一,代码如下:

    public int countOneBit(int number) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) 
            if ((number & (1 << i)) != 0) ++ans;
        return ans;
    }

解法2: 解法1遍历的次数固定32次,还有更好的方法吗?有
下面我们先看一下,假设一个数x不是0,x-1的二进制表示与x的二进制表示有什么不同???
其实是这样的,因为x不是0,因此不妨假设从低位开始数,x的第一个非0的位在第 n n n( 0 < = n < = 31 0<=n<=31 0<=n<=31)位,那么x和-1相加,相当于把前低n位全部取反,剩余的,同时第n位和1相加,产生一个进位,导致后面的位和1相加以后,都没有发生改变。如下图所示:
在这里插入图片描述
x-1的结果,就是把x最右边的1及其前面的位都取反了,那x&(x-1),相当于,把x最右边的1置0,那么我们设置一个计数器,每一次把x最右边的1置0,然后计数器加一,直到x变成0为止,代码如下:

    public int countOneBit(int number) {
        int ans = 0;
        while (number != 0) {
            number &= (number - 1);
            ++ans;
        }
        return ans;
    }

这个循环次数是,number中有几个1,就循环几次。

解法3: 有没有更好的做法?有,请往下看。
这种做法的思路是,先两位分组,把两位中1的数量找出来,还放到这两位中,如下图所示:
在这里插入图片描述
那如何做到这一步呢?两位二进制,一共四种情况,我们分别列出来看一下,如下图所示:

在这里插入图片描述
假设这个数是x,我们可以把x无符号右移一位,然后使用原来的x减去它,即x-(x>>>1);这样做并不可行,因为是整体无符号右移,不是两位一组的无符号右移,这样移动会导致下一组的最低位,移到这一组的最高位,如果这一位是0那道无所谓,如果是1,就影响计算了,因此,我们需要把x>>>1的值中所有的偶数位置0,因此只需要让它跟0x55555555相与即可,如下图所示:
在这里插入图片描述
经过这一步,已经完成了两位一组的,把1的个数找出来,并存储在这两位当中了,接下来,就是4位一组,8位一组,16位一组进行合并即可。

四位一组进行合并,我们需要四位一组的高两位和低两位相加,我们把上一步得到的结果,记作x,我们让x无符号右移两位,这样,高两位和低两位对齐了,不要忘记清除垃圾数据,因此右移的过程中,下一组的低两位,会移动到当前组的高两位上,影响计算结果,因此计算之前,清除高两位的值,如下图所示:
在这里插入图片描述
八位一组进行合并,跟四位一组合并类似,也是需要移位,把最高位置0,下面不再赘述,当16位一组合并完成后,因为int类型有32位,1的个数不会超过2的5次方,所以取结果的低6位即可,代码如下:

    public int countOneBit(int number) {
        number -= ((number >>> 1) & 0x55555555);
        number = (number & (0x33333333)) + ((number >>> 2) & 0x33333333);
        number = (number + (number >>> 4)) & 0x0f0f0f0f;
        number += number >>> 8;
        number += number >>> 16;
        return number & 0x3f;
    }

这其实是jdk中bitCount方法的实现。

2.8. 判断一个数是不是2的幂

如果一个整数是2的幂(如果是负数的话,说他的绝对值),那么它的二进制表示中只有1个位是1,我们只需要把它最右边的1置0,然后判断是否为0即可,代码如下:

    public boolean isPowerOf2(int number) {
        if (number == 0) return false;
        number = Math.abs(number);
        return (number & (number - 1)) == 0;
    }

2.9. 取一个数二进制表示中最右边的1表示的数

意思就是,例如12的二进制表示为0000 1100,取出最右边的1代表的数,0000 0100,即4,代码如下:

    public int getRightOneBit(int number) {
        return number & ((~number) + 1);
    }

number取反+1,相当于把number的最右边的1及其右边的0(如果存在的话)保留下来,最后边的1左边的位全部取反了,然后相与,就把最后边的1留下来了。
在这里插入图片描述(~number) + 1其实就是-number,可以简单记为number&-number

2.10. 取一个数二进制表示中最左边的1表示的数

这里只针对正整数x,例如12的二进制表示为0000 1100,取出最左边的1代表的数,0000 1000,即8,就是找到不超过x的最大的2的幂。例如给个7,返回4,给个9返回8,给个8返回8。
此时我们不知道最左边的1在哪个位置,其实方法就是我们把这个数,最左边的1右边全变成1,例如12,0000 1100,我们想办法变成0000 1111,这样我们把这个数+1,然后右移一位即可。如何做到把最左边的1右边全变成1呢?其实我们可以把x和(x>>>1)按位或,这样,最左边的1和次左边的位都变成1,然后右移两位,再次或,然后右移四位,八位,16位,代码如下:
在这里插入图片描述

    public int getLeftOneBit(int number) {
        if ((number & (number - 1)) == 0) return number;
        number |= number >>> 1;
        number |= number >>> 2;
        number |= number >>> 4;
        number |= number >>> 8;
        number |= number >>> 16;
        return (number + 1) >>> 1;
    }

2.11. 找出大于等于一个数的最小的2的幂

例如给一个5,大于等于5的最小的2的幂是8,因此返回8,给3,返回4,给8,返回8。
其实,这个数值,就是上一个问题《取一个数二进制表示中最左边的1表示的数》,在最后不要右移一位的值,代码如下:

    public int getLeftOneBit(int number) {
        if ((number & (number - 1)) == 0) return number;
        number |= number >>> 1;
        number |= number >>> 2;
        number |= number >>> 4;
        number |= number >>> 8;
        number |= number >>> 16;
        return number + 1;//这里不要右移一位
    }

2.12. 只出现一次的数字

leetcode第136题,异或有两个性质

  1. 相同的两个数异或为0
  2. 0跟任何数异或是自己

因此,我们可以用0,跟数组中的所以数异或一次,相同的两个数必然两两异或变为0,剩下的那个数即为所求,代码如下:

public int singleNumber(int[] nums) {
        int ans = 0;
        for(int i: nums)ans^=i;
        return ans;
}

升级版题目:给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。leetcode260题
假设那两个数是a,b。我们按照第一道题的做法,把所有元素异或一遍,得到的值肯定是a^b的结果(记作res),且不为0,我们如何把这两个数拆分出来呢?
思路这样:**res的结果不为0,则肯定某一位是1,我们不妨取res的最右边的1,那么a和b必然在这一位上一个是0,一个是1,我们把数组中这一位是1的所有数组挑出来再次异或一遍,最后得到的结果肯定就是a和b中的一个,记作ans,再把ans和res异或,得到另一个。**代码如下:

    public int[] singleNumber(int[] nums) {
        int res = 0;
        for (int n : nums) res ^= n;
        int masker = res & (~res + 1), ans = 0;
        for (int n : nums) 
            if ((n & masker) != 0) ans ^= n;
        return new int[]{ans, res ^ ans};
    }

以上内容如有错误,码字仓促,请大家不吝赐教。OK,今天的内容据到这里啦,下期再见,拜拜。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值