编程导航算法通关村第十一关 | 位运算的高频算法题

71 阅读5分钟

位移的妙用

位1的个数

. - 力扣(LeetCode)

有两种思路,让1不断左移或者将原始数据不断右移。例如将原始数据右移就是:

00000100100100010000110001000100
 & 00000000000000000000000000000001
 = 00000000000000000000000000000000

很明显此时就可以判断出第二位是0,然后继续将原始数据右移就可以依次判断出每个位置是不是1了。因此是不是1,计算一下(n>>i) & 1就可以了,所以代码顺理成章:

public int hammingWeight(int n) {
    int count = 0;
    for (int i = 0; i < 32; i++) {
        count += (n >> i) & 1;
    }
    return count;
}

按位与运算有一个性质:对于整数 n,计算n & (n−1) 的结果为将 n 的二进制表示的最后一个 1 变成 0。利用这条性质,令 n=n & (n−1),则 n 的二进制表示中的 1 的数量减少一个。重复该操作,直到 n 的二进制表示中的全部数位都变成 0,则操作次数即为 n 的位 1 的个数。

由此可以将上面的代码优化:

public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        n = n & (n - 1);
        count++;
    }
    return count;
}

比特位计数

. - 力扣(LeetCode)

最直观的方法是对从 0 到 num 的每个数直接计算"一比特数"。每个int 型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到1 的数目。

public int[] countBits(int n) {
    int[] bits = new int[n + 1];
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j < 32; j++) {
            bits[i] += (i >> j) & 1;
        }
    }
    return bits;
}

利用位运算的技巧,可以提升计算速度。按位与运算(&)的一个性质是:对于任意整数 x,令 x=x&(x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成0,则操作次数即为 x 的「一比特数」。

public int[] countBits(int num) {
    int[] bits = new int[num + 1];
    for (int i = 0; i <= num; i++) {
        bits[i] = countOnes(i);
    }
    return bits;
}

public int countOnes(int x) {
    int ones = 0;
    while (x > 0) {
        x &= (x - 1);
        ones++;
    }
    return ones;
}

颠倒无符号整数

. - 力扣(LeetCode)

我们注意到对于 n 的二进制表示的从低到高第 i 位,在颠倒之后变成第 31-i 位( 0≤i<32),所以可以从低到高遍历 n 的二进制表示的每一位,将其放到其在颠倒之后的位置,最后相加即可。

理解之后,实现就比较容易了。由于 Java不存在无符号类型,所有的表示整数的类型都是有符号类型,因此需要区分算术右移和逻辑右移,在Java 中,算术右移的符号是 >>,逻辑右移的符号是 >>>。

public int reverseBits(int n) {
    int reversed = 0, power = 31;
    while (n != 0) {
        reversed += (n & 1) << power;
        n >>>= 1;
        power--;
    }
    return reversed;
}

位实现加减乘除专题

位运算实现加法

. - 力扣(LeetCode)

两个位加的时候,我们无非就考虑两个问题:进位部分是什么,不进位部分是什么。从上面的结果可以看到,对于a和b两个数不进位部分的情况是:相同为0,不同为1,这不就是a⊕b吗?

而对于进位,我们发现只有a和b都是1的时候才会进位,而且进位只能是1,这不就是a&b=1吗?然后位数由1位变成了两位,也就是上面的[4]的样子,那怎么将1向前挪一下呢?手动移位一下就好了,也就是(a & b) << 1。所以我们得到两条结论:

  • 不进位部分:用a⊕b计算就可以了。
  • 是否进位,以及进位值使用(a & b) << 1计算就可以了。

于是,我们可以将整数 a 和 b 的和,拆分为 a 和 b 的无进位加法结果与进位结果的和,代码就是:

public int getSum(int a, int b) {
    while (b != 0) {
        int sign = (a & b) << 1;
        a = a ^ b;
        b = sign;
    }
    return a;
}

递归乘法

. - 力扣(LeetCode)

如果不让用*来计算,一种是将一个作为循环的参数,对另一个进行累加,但是这样效率太低,所以我们还是要考虑位运算。

首先,求得A和B的最小值和最大值,对其中的最小值当做乘数(为什么选最小值,因为选最小值当乘数,可以算的少),将其拆分成2的幂的和,即min = a_0 * 2^0 + a_1 * 2^1 + ... + a_i * 2^i + ...其中a_i取0或者1。其实就是用二进制的视角去看待min,比如12用二进制表示就是1100,即1000+0100。例如:

13 * 12 = 13 * (8 + 4) = 13 * 8 + 13 * 4 = (13 << 3) + (13 << 2);

上面仍然需要左移5次,存在重复计算,可以进一步简化:

假设我们需要的结果是ans,

定义临时变量:tmp=13<<2 =52计算之后,可以先让ans=52

然后tmp继续左移一次tmp=52<<1=104,此时再让ans=ans+tmp

这样只要执行三次移位和一次加法,实现代码:

public int multiply(int A, int B) {
    int min = Math.min(A, B);
    int max = Math.max(A, B);
    int ans = 0;
    for (int i = 0; min != 0; i++) {
    //位为1时才更新ans,否则max一直更新
        if ((min & 1) == 1) {
            ans += max;
        } 
        min >>= 1;
        max+=max;
    }
      return ans;
}