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. 学习位运算的意义

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

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

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

相关推荐
Dream_Snowar11 分钟前
速通Python 第四节——函数
开发语言·python·算法
星河梦瑾13 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富16 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想18 分钟前
JMeter 使用详解
java·jmeter
言、雲21 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
Altair澳汰尔24 分钟前
数据分析和AI丨知识图谱,AI革命中数据集成和模型构建的关键推动者
人工智能·算法·机器学习·数据分析·知识图谱
TT哇28 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A1 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
Python机器学习AI1 小时前
分类模型的预测概率解读:3D概率分布可视化的直观呈现
算法·机器学习·分类
Yvemil71 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java