算法通关村第十一关——位运算的高频算法题(白银)

算法通关村第十一关——位运算的高频算法题(白银)

    • 1 位移的妙用
      • 1.1 位`1`的个数
        • 解法1(easy)
        • 解法2(优化1)
        • 解法3(Integer.bitCount()源码)
      • 1.2 比特位计数
        • 解法1(easy)
        • 解法2(动态规划)
      • 1.3 颠倒二进制位
        • 解法1(easy)
        • 解法2(分治)离谱。。
    • 2 位实现加减乘除专题
      • 2.1 两整数之和
      • 2.2 递归乘法
        • 解法1(直接相加,超时!)
        • 解法2(从低到高逐位处理)
        • 解法3 (迭代)

1 位移的妙用

1.1 位1的个数

leetcode 191. 位1的个数

解法1(easy)

首先我们可以根据题目要求直接计算,题目给定的 n 是 32 位二进制表示下的一个整数,计算位 1 的个数的最简单的方法是遍历 n 的二进制表示的每一位,判断每一位是否为 1,同时进行计数。

那问题就是如何通过位运算来识别到1,例如:

00001001001000100001100010001001,首先我们注意到要识别到最低位的1,可以这么做:

00001001001000100001100010001001
 & 00000000000000000000000000000001
 = 00000000000000000000000000000001

也就说将原始数字和1进行&运算就能知道最低位是不是1了,那其他位置怎么算呢?

我们可以有两种思路,让1不断左移或者将原始数据不断右移。例如将原始数据右移,再进行计算

00000100100100010000110001000100
 & 00000000000000000000000000000001
 = 00000000000000000000000000000000

很明显此时就可以判断出第二位是0,然后继续将原始数据右移就可以依次判断出每个位

置是不是1了。因此是不是1,计算一下(n>>i) & 1就可以了,所以代码顺理成章:

public class Solution {
    public int hammingWeight(int n) {
        int count = 0;
        for(int i=0; i<32; i++){
            count += (n >> i) & 1;
        }
        return count;
    }
}
解法2(优化1)

观察这个运算:n & (n - 1),其运算结果恰为将 n 的二进制位中的最低位的 1 变为 0 后的结果。

例如:6 & (6 - 1) = 4,6 = (110)₂,4 = (100)₂。运算结果 4 即为将 6 的二进制位中的最低位的 1 变为 0 后的结果。

我们可以利用这个位运算的性质加速我们的检查过程。在实际代码中,我们不断让当前的 n 与 n - 1 做与运算,直到 n 变为 0 即可。因为每次运算会使得 n 的最低位的 1 被翻转,所以运算次数等于 n 的二进制位中 1 的个数。

public class Solution {
    public int hammingWeight(int n) {
        int count = 0;
        while(n != 0){
            n &= n-1;
            count++;
        }
        return count;
    }
}
解法3(Integer.bitCount()源码)

Integer.bitCount()源码通过多次迭代和位移操作,将整数 n 分成多个小组,并分别统计每个小组中包含的 1 的个数。

public class Solution {
    public int hammingWeight(int n) {
        n = n - ((n >>> 1) & 0x55555555);
        n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
        n = (n + (n >>> 4)) & 0x0f0f0f0f;
        n = n + (n >>> 8);
        n = n + (n >>> 16);
        return n & 0x3f;
    }
}

详解如下:

Integer.bitCount()方法通过一系列位运算操作来计算给定整数 n 的二进制表示中包含的 1 的个数。以下是该方法的源码,并对其中的每个步骤进行解释:

public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}
  • 首先,i = i - ((i >>> 1) & 0x55555555);这一行使用右移和按位与操作,将整数 n 每两位一组分组,然后将每组中最低位的 1 变为 0。这样,每两位一组的结果中包含的 1 的个数就减少了一半。

  • 接下来,i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);这一行再次使用右移和按位与操作,将每四位一组分组,然后将每组中包含的 1 的个数相加。这样,每四位一组的结果中包含的 1 的个数又减少了一半。

  • 继续进行下去,i = (i + (i >>> 4)) & 0x0f0f0f0f;将每八位一组分组,并将每组中包含的 1 的个数相加。这一步操作使得每八位一组的结果中包含的 1 的个数减少到 8 位之内。

  • 然后,i = i + (i >>> 8);将每十六位一组分组,并将每组中包含的 1 的个数相加。这一步操作使得每十六位一组的结果中包含的 1 的个数减少到 16 位之内。

  • 最后,i = i + (i >>> 16);将整个 32 位整数分成两个 16 位的小组,并将它们的结果相加。

  • 最终,通过return i & 0x3f;对结果进行与运算,只保留最低的 6 位数,即为给定整数 n 的二进制表示中包含的 1 的个数。

总而言之,Integer.bitCount()方法利用了位运算和分组统计的技巧,通过多次迭代和按位操作,将整数 n 分组并统计每组中包含的 1 的个数。这种分组统计法能够在较少的步骤中得到准确的结果,从而提高了计算速度。此外,Integer.bitCount()方法是经过优化的标准库函数,其底层实现进一步优化和调整,保证了其性能的高效性。

1.2 比特位计数

leetcode 338. 比特位计数

解法1(easy)

这道题没什么好讲的,就是上面那一题的变式,只需要加多一层遍历即可

class Solution {
    public int[] countBits(int n) {
        int[] res = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            int count = 0;
            int num = i;
            while (num != 0) {
                num &= num - 1;
                count++;
            }
            res[i] = count;
        }
        return res;
    }
}
解法2(动态规划)

先简单了解一下什么是动态规划

动态规划(Dynamic Programming)是一种通过将问题分解成子问题并存储子问题的解以避免重复计算的方法。

它的基本思想是将原问题拆解成若干个子问题,先求解子问题的最优解,然后根据子问题的最优解推导出原问题的最优解。在求解子问题的过程中,动态规划会使用一个表格或数组来记录已经求解过的子问题的结果,以便在需要时直接查找而不需要重新计算。

动态规划主要用于求解具有重叠子问题和最优子结构性质的问题。其中,重叠子问题指的是子问题之间存在重复计算,而最优子结构指的是原问题的最优解可以由子问题的最优解推导出来。

动态规划适用于一类具有无后效性的问题,即某个阶段的状态一旦确定,就不会受到后面决策的影响。常见的应用情景包括最短路径问题、背包问题、编辑距离问题等。

那么再看这一题,我们可以观察到一个规律:

对于一个数字x,其二进制中1的个数与x/2的二进制中1的个数是有关系的。

  • 如果x是偶数,那么它的二进制中1的个数与x/2的二进制中1的个数相同;

  • 如果x是奇数,那么它的二进制中1的个数是x/2的二进制中1的个数加1。

利用这个规律,我们可以使用动态规划来逐步计算每个数字的二进制中1的个数,减少重复计算。

class Solution {
    public int[] countBits(int n) {
        int[] res = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            res[i] = res[i/2] + i % 2;
        }
        return res;
    }
}

1.3 颠倒二进制位

190. 颠倒二进制位

解法1(easy)

这题的解决思想是:怎么像String一样,拿到第一个数,然后放在最后

  • 二进制位拿到第一个数可以这样做:n & 1

  • 放在最后,就是进行向左移动:(n & 1) << 31

输入:n = 00000010100101000001111010011100
经过计算:(n & 1) << 31
输出:n = 00000000000000000000000000000000
    
输入:n = 00000010100101000001111010011101
经过计算:(n & 1) << 31
输出:n = 10000000000000000000000000000000

两个例子的输入,低位的第一位,一个是0,一个是1,

那么要实现颠倒,就需要一个循环,然后,每次都能够拿到低位,然后放在高位对应的位置,再进行相加即可

public class Solution {
    public int reverseBits(int n) {
        int reversed = 0, power = 31;
        while(n != 0){
            reversed += (n & 1) << power;
            n >>>= 1;
            power--;
        }
        return reversed;
    }
}
解法2(分治)离谱。。

这个方法主要是进行拆分,分为5步来解决

  • 第一个步骤:将奇数位和偶数位进行交换。
  • 第二个步骤:将每两位中的前两位和后两位进行交换。
  • 第三个步骤:将每四位中的前四位和后四位进行交换。
  • 第四个步骤:将每八位中的前八位和后八位进行交换。
  • 最后一个步骤:将整个32位二进制表示进行颠倒。。
public class Solution {
    private static final int M1 = 0x55555555; // 01010101010101010101010101010101
    private static final int M2 = 0x33333333; // 00110011001100110011001100110011
    private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
    private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111

    public int reverseBits(int n) {
        n = n >>> 1 & M1 | (n & M1) << 1;
        n = n >>> 2 & M2 | (n & M2) << 2;
        n = n >>> 4 & M4 | (n & M4) << 4;
        n = n >>> 8 & M8 | (n & M8) << 8;
        return n >>> 16 | n << 16;
    }
}

2 位实现加减乘除专题

2.1 两整数之和

leetcode 371. 两整数之和

既然不能使用+和-,那只能使用位运算了。我们看一下两个二进制位相加的情况:

[1] 0 + 0 = 0
[2] 0 + 1 = 1
[3] 1 + 0 = 1
[4] 1 + 1 = 0 (发生了进位,应该是10的)

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

算法通关村第十一关——位运算的高频算法题(白银)_第1张图片

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

算法通关村第十一关——位运算的高频算法题(白银)_第2张图片

所以我们得到两条结论:

  • 不进位部分:用a⊕b计算就可以了。

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

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

2.2 递归乘法

leetcode 面试题 08.05. 递归乘法

解法1(直接相加,超时!)

小时候都学过,相乘其实就是加法:5*2 等价于 5+5

所以我们可以通过上面的两数之和的方式就行修改就可以:

class Solution {
    public int multiply(int A, int B) {
        if(A == 0 || B == 0){
            return 0;
        }
        if(A == 1 || B == 1){
            return A == 1 ? B : A;
        }
        int n = A;
        for(int i=1; i<B; i++){
            int m = A;
            while(n != 0){
                int c = (n & m) << 1;
                n = n ^ m;
                m = c;
            }
            n = m;
        }
        return n;
    }
}
解法2(从低到高逐位处理)

首先,求得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

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

class Solution {
    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++){
            if((min & 1) == 1){
                ans += max;
            }
            min >>= 1;
            max += max;
        }
        return ans;
    }
}
解法3 (迭代)

跟上面那题的方式差不多,效率是一样的

class Solution {
    public int multiply(int A, int B) {
        if (A == 0 || B == 0) {
            return 0;
        }
        
        int result = 0;
        while (B != 0) {
            if ((B & 1) != 0) {
                result += A;
            }
            A <<= 1;
            B >>= 1;
        }
        
        return result;
    }
}

这两个算法都是使用位运算实现两数相乘。

第一个算法使用了循环从低位到高位逐位处理,并利用位运算判断当前位是否为1,然后将结果累加到ans中。

第二个算法使用了迭代方法,每次将A左移一位,B右移一位,同时检查B的最低位是否为1,如果是则将A加到result中。

根据代码分析,第一个算法的时间复杂度为O(log(min(A, B))),第二个算法的时间复杂度为O(log(max(A, B)))。因此,第一个算法的效率更高,尤其当min(A, B)的值比较小的时候,第一个算法的效率会更明显地优于第二个算法。

你可能感兴趣的:(数据结构,算法,算法,java,数据结构,笔记)