class 031 位运算的骚操作

这篇文章是看了"左程云"老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。

这个是"左程云"老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.

左程云的个人空间-左程云个人主页-哔哩哔哩视频 (bilibili.com)

1. 题目一:判断一个整数是不是 2 的幂

1.1 2 的幂的定义是什么?

只有在二进制下, 任何位上只有一个 1, 才能是 2 的幂, 比如 0000 0001, 0001 0000, 都是 2 的幂.


要让两个2的幂相加后仍然等于2的幂,需要满足以下条件:

  1. 两个数必须是2的幂:即它们的形式必须是 (2^n) 和 (2^m),其中 (n) 和 (m) 是非负整数。

  2. 两个数的指数必须相同:即 (n = m)。


但是二进制下, 一个"二进制"位置上只能有一个是 1, 所以所有"二进制位 "上只有一个 1, 才能是 2 的幂,

1.1.1 逻辑实现

"题目"的解法是:只要将最右侧的 1 提取出来, 判断是不是与本身相等就行

1.1.2 举例说明

0000 0001 这个是 2 的 0 次方 == 1; 0000 0011 这个是 2 的 0 次方加上 2 的 1 次方 == 3; 将最右侧的 1 提取出来是:0000 0001, 1 != 3; 所以 3 不是 2 的幂;

1.1.3 Brian Kernighan 算法

作用:提取出一个数字的二进制位置下的最右侧的"1".

Java 复制代码
n & -n  // 最后可以将n这个数字的最右侧的1提取出来.

1.1.4 代码实例

Java 复制代码
// 需要会 Brian Kernighan 算法  
// 提取出二进制里最右侧的1;  
// 判断一个整数是不是2的幂  
class Solution {  
    public boolean isPowerOfTwo(int n) {  
        return n > 0 && n == (n & -n);  
    }  
}

2. 题目二:判断一个整数是不是 3 的幂

如果一个数字是3的某次幂,那么这个数一定只含有3这个质数因子

1162261467是int型范围内,最大的3的幂,它是3的19次方

这个1162261467只含有3这个质数因子,如果n也是只含有3这个质数因子,那么

1162261467 % n == 0

反之如果1162261467 % n != 0 说明n一定含有其他因子

Java 复制代码
public static boolean isPowerOfThree(int n) {  
    return n > 0 && 1162261467 % n == 0;  
}

3. 题目三:>= n 最小的 2 的幂

3.1 题目描述

已知n是非负数

返回大于等于n的最小的2某次方

如果int范围内不存在这样的数,返回整数最小值

比如说:输入的 n 是 13, 那最后返回的结果是:16, 若是输入的 n 是 4, 返回的结果是 4.

若是 n <= 0, 就直接返回 1 (2 的零次方).

3.2 解法

因为这个直接涉及到了位运算, 直接结合代码进行解释.(下面的代码可以直接复制之后直接运行)

代码实现的意义:将一个数字 n, 然后将 n - 1 这个数字的最左侧的二进制位置的 1 之后的二进制位置全部修改为 1, 然后 + 1, 这样能返回一个 2 的幂.

举一个例子:

Java 复制代码
假设:n == 45714
00000000 00000000 10110010 10010010 这个数字是:45714, 十进制表示.
先将n--;
00000000 00000000 10110010 10010001 这个数字是:45713, 十进制表示.

n >>> 1 的结果是:
00000000 00000000 01  0110010 1001000 不用管这个数字的结果是多少.不关心十进制表示.

将 n |= n >>> 1 的结果是:
00000000 00000000 11  110010 10010001 我们不关心之后的二进制位置上的数字, 因为之后的数字后续都会变成1, 对这个来说没有什么意义. 此时我们将最左侧的一个 1 右边的一个二进制位置的数字修改为了 1 , 此时最左侧和最左侧右边的一个位置的数字变成了 1 ,然后我们继续实现.

n >>> 2 的结果是:
00000000 00000000 00  111100 10100100

n |= n >>> 2 的结果是:(因为我们上一步已经将两个二进制位置的数字修改为1了, 所以此时我们移动两个位置.)
00000000 00000000 1111  0010 10010001  我们还是不关心后续二进制位置上的数字, 和原来的原因一样.

n >>> 4 的结果是:
00000000 00000000 0000  1111 00101001

将 n |= n >>> 4 的结果是:
00000000 00000000 11111111   10010001

n >>> 8 的结果是:
00000000 00000000 00000000 11111111

n |= n >>> 8 的结果是:
00000000 00000000 11111111 11111111

这样最后的n |= n >>> 16 就没有必要了, 因为:n |= n >>> 16实现之后还是原来的结果. 

00000000 00000000 11111111 11111111 
最后将这个数字 + 1.
00000000 00000001 00000000 00000000 这个就是最后的结果.这个肯定是一个2 的幂, 因为在32个二进制位置中, 只有一个1, 前面的题目也说过这个问题了.

此时先进行n--的意义也应该是有了深入的理解, 因为我们希望输入的数字本身就是2的幂的情况下, 返回这个输入的数字本身, 而不是一个更大的数字.
Java 复制代码
public class Code03_Near2power {  
  
    public static final int near2power(int n) {  
       if (n <= 0) {  
          return 1;  // 若是n <= 0, 就直接返回1 (2 的零次方).
       }  
       n--;     // 先将n--, 目的是为了能让本身就是2的幂的数最终返回自己, 比如输入4, 返回的值还是4.
       n |= n >>> 1;  
       n |= n >>> 2;  
       n |= n >>> 4;  
       n |= n >>> 8;  
       n |= n >>> 16;  
       return n + 1;  
    }  
  
    public static void main(String[] args) {  
       int number = 13;  
       System.out.println(near2power(number));  
    }  
  
}

4. 题目四:范围内所有数字 & 的结果

4.1 题目描述

4.2 题目解法(位运算解法)

这个暴力方法肯定是谁都会, 就不写了, 而且这个暴力方法本来也没有任何意义.

使用位运算的解法:直接结合代码进行讲解了.

注意:二进制的加减法和十进制的加减法是一一对应的, 没有任何区别. 比如:

Java 复制代码
二进制:            十进制:
01010011           19999
01000000 -         09000 -
--------           -----
00010011           10999
Java 复制代码
我们此时假设有一个数字, 此时这个数字是 right
0101001101  right 

假设此时 right == left, 那这个数字就可以直接返回了, 因为只有一个数字, 没办法 & 运算.

假设此时 right > left, 那这个数字最右侧的 1 肯定是留不下了(将来肯定会变成 0 ),
因为将 right - 1, 这个数字肯定是在(left ~ right)这个范围的.
0101001100  right - 1, 将 right 和 right - 1 做 & 运算, 最右边的一个二进制位置的结果肯定是 0 .

利用 Brian Kernighan 算法, 将 right - right & (-right), 此时保留结果.
0101001100  这个是结果. 前面的 1 都能留下来.判断一下和 left的关系, 要是 > left 继续往下走, 要是 <= left 就停止.

此时假设 right 还是 > left, 那前面结果最右侧的 1 肯定是留不下了(将来肯定会变成 0 ),
还是按照上面的方式将此时的结果 - 1
0101001011 这个是 上面的结果 - 1 的值. 这个值肯定在(left ~ right)范围内, 和上面的数字做 & 运算,最后的结果肯定是将后面两个 1 消除掉了.

所以此时将 right - right & (-right), 还是使用了 Brian Kernighan 算法, 
0101001000 这个是结果.

总结:步骤就是在 right > left 的情况下, 将 right 在二进制状态下最右侧的二进制的 1 减掉, 此时继续判断 right 和 left 的关系, 要是 right > left, 就继续将 right 在二进制状态下最右侧的二进制的 1 减掉, 要是 right <= left, 就直接停止, 返回 right. 然后继续判断 right 和 left

的关系, 其中使用到了 Brian Kernighan 算法 和 二进制的加减. 都在上面有说明.

Java 复制代码
public static int rangeBitwiseAnd(int left, int right) {  
    while (left < right) {  
       right -= right & -right;  
    }  
    return right;  
}

注意:题目 5, 6 都是大牛的实现, 所以我们只需要了解一下, 然后记住, 当成一个模板使用就行了

5. 题目五:逆序二进制的状态

5.1 题目描述

5.2 解法

当然可以直接用 for循环和数组, 然后一个一个地将所有二进制位置进行记录, 最后利用 | 运算返回逆序之后的数字.

5.2.1 暴力解法

这个的实现效率很慢, 所以知道就行了, 不用记住.

Java 复制代码
public static int reverseBit(int n) {  
    int[] cnts = new int[32];  
  
    for (int i = 0; i < 32; i++) {  
       cnts[i] = (n & (1 << i)) != 0 ? 1 : 0;  
    }  
  
    int ans = 0;  
    for (int i = 31; i >= 0; i--) {  
       ans |= cnts[31 - i] == 1 ? 1 << i : 0;  
    }  
    return ans;  
}

5.2.2 代码实例

看一下大牛的实现:这个是需要记住的. 将来直接使用就行了

Java 复制代码
public static int reverseBits(int n) {  
    n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);  
    n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);  
    n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);  
    n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);  
    n = (n >>> 16) | (n << 16);  
    return n;  
}

5.2.3 逻辑实现

我们先使用 8 个二进制位置进行说明:abcd efgh.

  1. 先进行 1 VS 1 的翻转:badc fehg.
  2. 然后进行 2 VS 2 的翻转:dcba hgef.
  3. 然后进行 4 VS 4 的翻转:hgef dcba.
  4. 然后可以将其拓展到 int(32位) 的情况下.

具体说明如何实现:如何将 1 VS 1 实现翻转:依然是:abcd efgh.

  1. 先将 abcd efgh 按位与 &10101010 这样最后的结果是:a0c0 e0g0.
  2. 然后将 abcd efgh 按位与 &01010101 这样最后的结果是:0b0d 0f0h.
  3. a0c0 e0g0 进行 >>> 1 运算, 最后结果是:0a0c0e0g
  4. 0b0d 0f0h 进行 << 1 运算, 最后结果是:b0d0f0h0
  5. 最后将两个结果进行 按位或( | ) 运算, 最后结果:badc fehg.
  6. 因为我们使用的是 8 个二进制位的, 将其扩展到 32 个二进制位, 1010 对应的是:十六进制的a, 0101 对应的是 十六进制的 5. 所以扩展到 32 位是:
    n = ((n & 0 xaaaaaaaa) >>> 1) | ((n & 0 x 55555555) << 1);.

如何实现:2 VS 2 的翻转:此时 n 的状态是:badc fehg.

  1. 先将 badc fehg 按位与 &11001100, 最后结果是:ba00 fe00.
  2. 然后将 badc fehg 按位与 &00110011, 最后结果是:00dc 00hg.
  3. 然后将 ba00 fe00 进行 >>> 2 操作, 结果:00ba 00fe.
  4. 然后将 00dc 00hg 进行 << 2 操作, 结果:dc00 hg00.
  5. 最后进行按位或 ( | ) 运算, 结果:dcba hgfe.
  6. 因为我们使用的是 8 个二进制位, 所以扩展到 32 个二进制位, 1100 对应的十六进制是:c, 0011 对应的十六进制是:3.

如何实现:4 VS 4 的翻转:此时 n 的状态是:dcba hgfe.

  1. 先将 dcba hgfe 按位与 &1111 0000, 最后结果是:dcba 0000.
  2. 然后将 dcba hgfe 按位与 &0000 1111, 最后结果是:0000 hgfe.
  3. 然后将 dcba 0000 进行 >>> 4 操作, 结果:0000 dcba.
  4. 然后将 0000 hgfe 进行 << 4 操作, 结果:hgfe 0000.
  5. 最后进行按位或 ( | ) 运算, 结果:hgfe dcba.
  6. 因为我们使用的是 8 个二进制位, 所以扩展到 32 个二进制位, 1111 对应的十六进制是:f, 0000 对应的十六进制是:0.

之后的 8 VS 8 的就不进行说明了, 经过前面的推导, 后续的实现肯定是能进行的, 自己画一下吧.

6. 二进制中有几个 1

6.1 逻辑实现

我们还是按照 8 个二进制位进行举例子:1111 1001, 我们定义一个长度, 统计每一个长度中的 1 的个数, 假设现在长度是 1,

  1. 那么 0 位置的 1 个数有 1 个,
  2. 1 位置 1 的个数有 0 个,
  3. 2 位置 1 的个数有 0
  4. 3 位置 1 的个数有 1
  5. 4 位置 1 的个数有 1
  6. 5 位置 1 的个数有 1
  7. 6 位置 1 的个数有 1
  8. 7 位置 1 的个数有 1

然后我们进行扩展, 将现在的长度变为:2, 那么:11 11 10 01

  1. 1 位置 1 的个数有 1 个,
  2. 2 位置 1 的个数有 1
  3. 3 位置 1 的个数有 2
  4. 4 位置 1 的个数有 2

用代码实现将 1 长度变为 2 长度

Java 复制代码
先将 1111 1001 & 0101 0101
最后结果是:01010001

然后我们将 1111 1001 >>> 1 -> 0111 1100 & 0101 0101
最后结果是:01010100

然后将两个状态相加:
1010 0101   此时就成了长度为 2 的情况下, 二进制中 1 的个数
10 10 01 01 
2  2  1  1   长度为 2 的情况下, 二进制中 1 的个数

然后继续使用代码表示将 2 长度迁移到 4 长度

Java 复制代码
1010 0101 这个是长度为 2 的情况下的表示

我们将 1010 0101 & 0011 0011
0010 0001 结果

然后我们将 1010 0101 >>> 2 -> 0010 1001 & 0011 0011
0010 0001 结果

将两个结果加起来
0100 0010     此时是长度为 4 的情况下, 二进制中 1 的个数

然后继续用代码表示将 4 长度迁移到 8 长度

Java 复制代码
0100 0010 这个是长度为 4 的情况下的表示

我们将 0100 0010 & 0000 1111
0000 0010 结果

然后将 0100 0010 >>> 4 -> 0000 0100 & 0000 1111
0000 0100 结果

将两个结果相加:
0000 0110 这个是长度为 8 的情况下, 二进制中 1 的个数,此时是:6 个.(2^2 + 2^1 == 6).

以此类推, 一直将长度迁移到了 int(32位) 的情况下就是最后的结果

6.2 代码实例

Java 复制代码
public static int cntOnes(int n) {  
    n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);  迁移到长度为 2
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);  迁移到长度为 4 
    n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);  迁移到长度为 8
    n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);  迁移到长度为 16
    n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); 迁移到长度为 32, 就是最后结果
    return n;  最后结果
}

另一个代码实例:这个用了 Brian Kernighan算法, 这个题目让统计一个数字中 32个 二进制位中所有 1 的数量,

  1. 所以我们直接将数字中最右侧的 1 提取出来, 然后用原来的数字减掉, 此时设置一个计数器 cntscnts++,
  2. 然后继续利用 Brian Kernighan算法, 继续减掉, 直到数字变成 0 停止, 这样 cnts 的值就是一个数字中所有二进制位中 1 的数量.
Java 复制代码
public static int cntOnes(int n) {  
    int cnts = 0;  
  
    while (n != 0) {  
       n -= (n & -n);  
       cnts++;  
    }  
  
    return cnts;  
}

7. 学习位运算的意义

位运算的常数时间是非常好的, 使用位运算可以很大程度上提高我们代码的运行速度, 而且使用的内存也很少, 在一些底层的操作上, 使用位运算是极好的. 而且位运算使用起来也会很简洁高效,

但是我们也没有必要强制使用位运算, 没有必要去在任何情况下都追求位运算的实现和使用, 尽量写到时间复杂度和空间复杂度最优, 自己能理解就行. 不要钻牛角尖.

大牛的实现我们直接当成模板用就行了,

相关推荐
尚学教辅学习资料4 分钟前
基于SSM+uniapp的营养食谱系统+LW参考示例
java·uni-app·ssm·菜谱
passer__jw7675 分钟前
【LeetCode】【算法】208. 实现 Trie (前缀树)
算法·leetcode
shenweihong9 分钟前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
张保瑞13 分钟前
十五:java web(7)-- Spring Boot
java·spring boot
stm 学习ing14 分钟前
C语言 循环高级
c语言·开发语言·单片机·嵌入式硬件·算法·嵌入式实时数据库
sniper_fandc33 分钟前
抽象工厂模式
java·设计模式·抽象工厂模式
芦半山1 小时前
Android“引用们”的底层原理
android·java
2401_857636391 小时前
Spring Boot环境下的知识分类与检索
java·spring boot·后端
阑梦清川1 小时前
数学建模启发式算法篇(一)---遗传算法
算法·数学建模·启发式算法