一文带你入门位运算

🎯 本文介绍了位运算的基础概念及其应用。位运算是直接对二进制位进行操作的高效运算方式,包括按位与(&)、或(|)、异或(^)、取反(~)、左移(<<)和右移(>>)。文章通过具体例子说明了位运算在判断奇偶、状态标记、数据压缩、快速计算以及找唯一数等场景中的实际用途。例如,利用位运算可以实现无需临时变量的数值交换,或者将多个小数字打包存储以节省空间。这些技巧不仅提升了程序性能,还在低级操作中发挥了重要作用。

位运算是什么?

位运算是计算机科学中一种直接对整数在内存中的二进制位进行操作的运算方式。它通过按位处理数据,可以高效地实现某些逻辑和数学操作。

常见的位运算符包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)。这些运算符可以直接操作二进制位,因此在需要高性能和低级操作的场景中非常有用。

二进制是什么?

计算机中所有数据以二进制形式存储(0和1),例如:

  • 十进制 5 → 二进制 101
  • 十进制 3 → 二进制 011
  • 十进制 -8 → 二进制(补码)1111 1111 1111 1111 1111 1111 1111 1000(以32位为例)

-8 的32位补码计算过程:

  1. 先写出 8 的二进制:

    Plain 复制代码
    8的二进制:0000 0000 0000 0000 0000 0000 0000 1000
  2. 取反(按位取反):

    Plain 复制代码
    取反后:1111 1111 1111 1111 1111 1111 1111 0111
  3. 加1得到 -8 的32位补码表示:

    Plain 复制代码
    加1后:1111 1111 1111 1111 1111 1111 1111 1000

基础位运算符

按位与(AND)&

【规则】

两位都为1时结果为1,否则为0。

【例子】

计算5 & 3

  • 5的二进制:101
  • 3的二进制:011
  • 5 & 3的结果:001(十进制为1)

常见用途

作用一:判断奇偶:x & 1 → 0是偶数,1是奇数

  • 二进制中,最低位(最右边的一位)决定了一个数是奇数还是偶数:
    • 如果最低位是 1,说明是奇数。
    • 如果最低位是 0,说明是偶数。
  • x & 1 的作用是提取最低位:
    • 1 的二进制是 0001,因此与任何数进行 & 操作时,只有最低位会被保留,其他位都会被清零。

作用二:清零某些位:x & 0b11111011 可以清除第3位

  • 二进制中,每一位代表一个权值(从右到左,第0位、第1位、第2位......)。
  • 0b11111011 的第3位是 0,其他位是 1
  • x0b11111011进行 & 操作时:
    • 第3位会被强制清零(因为 0 & 任何数 = 0)。
    • 其他位保持不变(因为 1 & 任何数 = 任何数)。

注意:ob作为开头,用来表示该数字为二进制表示

java 复制代码
public static void main(String[] args) {
    int x = 6;
    x = x & 0b11111011;
    // 输出 2
    System.out.println(x);
}

按位或(OR)|

【规则】

两位中有一个为1时结果为1。

【例子】

5 | 3 -> 101 | 011 = 111(十进制7)

常见用途

设置某些位为1:如x | 001000 可以设置第4位变为1。

按位异或(XOR)^

【规则】

两位不同时结果为1,否则为0。

【例子】

5 ^ 3 -> 101 ^ 011 = 110(十进制6)

  • x ^ x = 0:任何数和自己异或结果是0。

    Plain 复制代码
    # 假设 x = 7,二进制为 0111
    x ^ x = 0111 (7)
           ^ 0111 (7)
           ----
           = 0000 (0)
  • x ^ 0 = x:任何数和0异或结果是它本身。

    Plain 复制代码
    # 假设 x = 7,二进制为 0111
    x ^ 0 = 0111 (7)
           ^ 0000 (0)
           ----
           = 0111 (7)

常见用途

交换两个数:a ^= b; b ^= a; a ^= b;(无需临时变量)。

  1. a ^= b:将 ab 的异或结果赋值给 a
  2. b ^= a:将 b 和新的 a 异或,结果赋值给 b(此时 b 的值已经是原始的 a)。
  3. a ^= b:将 a 和新的 b 异或,结果赋值给 a(此时 a 的值已经是原始的 b)。

假设 a = 5 → 0101b = 3 → 0011

步骤1a ^= b

Plain 复制代码
a = a ^ b
  = 0101 (5)
  ^ 0011 (3)
  ----
  = 0110 (6)
此时 a = 6,b = 3。

步骤2b ^= a

Plain 复制代码
b = b ^ a
  = 0011 (3)
  ^ 0110 (6)
  ----
  = 0101 (5)
此时 a = 6,b = 5。

步骤3a ^= b

Plain 复制代码
a = a ^ b
  = 0110 (6)
  ^ 0101 (5)
  ----
  = 0011 (3)
此时 a = 3,b = 5,两个数的值已经交换。

在 java 中可以使用该方式来进行值交换

java 复制代码
public static void main(String[] args) {
    int a = 6, b = 3;
    a ^= b;
    b ^= a;
    a ^= b;
    // 输出 3 6        
    // System.out.println(a + " " + b);
}

经过实测使用位运算进行交换,效率更高

java 复制代码
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
        for (int j = 0; j < 10; j++) {
            int a = 6, b = 3;
            a ^= b;
            b ^= a;
            a ^= b;
        }
    }
    System.out.println("位运算交换,时间:" + (System.currentTimeMillis() - start) + "ms");

    start = System.currentTimeMillis();
    int temp;
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
        for (int j = 0; j < 10; j++) {
            int a = 6, b = 3;
            temp = b;
            b = a;
            a = temp;
        }
    }
    System.out.println("常规交换,时间:" + (System.currentTimeMillis() - start) + "ms");
}

输出

java 复制代码
位运算交换,时间:1ms
常规交换,时间:34ms

Process finished with exit code 0

按位非(NOT)~

【规则】

所有位取反(0变1,1变0)。

【例子】

~5:5的二进制:0000 0101(假设8位),取反后:1111 1010 = 十进制-6(补码表示法)

【注意】

结果与位数有关,32位和64位安位非结果不同。

左移 <<

【规则】

所有位向左移动,右侧补0。

【例子】

5 << 2理解为: 5的二进制:101,左移2位:10100(等于十进制20)

【常见用途】

快速乘以2的幂(x << n = x × 2ⁿ)。

右移 >>(带符号右移)

【规则】

所有位向右移动,左侧补符号位(正数补0,负数补1)。

【例子】

  • 8 >> 110000100(十进制4)
  • -8 >> 111111111 11111111 11111111 11111000(32位)→ 右移后补1 → 结果-4。

无符号右移 >>>(如Java支持)

【规则】

所有位向右移动,左侧补0。

【例子】

-8 >>> 1 → 在32位中结果为 2147483644(不再保留符号)。

步骤1:将 -8 表示为32位二进制

  • 在计算机中,负数的二进制采用 补码 表示,即1111 1111 1111 1111 1111 1111 1111 1000

步骤2:无符号右移 >>> 1

  • 将所有位向右移动1位,左侧补0。

  • 原始值:

    Plain 复制代码
    1111 1111 1111 1111 1111 1111 1111 1000
  • 右移1位:

    Plain 复制代码
    0111 1111 1111 1111 1111 1111 1111 1100
    • 注意:最左侧补0,而不是补符号位。

步骤3:将结果转换为十进制

  • 右移后的二进制:

    Plain 复制代码
    0111 1111 1111 1111 1111 1111 1111 1100
  • 转换为十进制:

    • 最高位是0,所以是正数。

    Plain 复制代码
    2^30 + 2^29 + 2^28 + ... + 2^2
    = 1073741824 + 536870912 + 268435456 + ... + 4
    = 2147483644
    
    所以结果是 2147483644
  • 如果是带符号右移 -8 >> 1:右移时,左侧补符号位(负数补1)。结果:

    Plain 复制代码
    1111 1111 1111 1111 1111 1111 1111 1100
    转换为十进制:-4。

常用场景

快速计算

位运算的速度通常比传统的算术运算快,因为它直接操作二进制位,在如下场景可以使用位运算进行加速:

  • 判断奇偶if (x & 1) == 0 ,说明 x 是偶数
  • 乘除2的幂x << 3 = x × 2^3 =x × 8;x >> 2 = x / 2^2 = x ÷ 4(向下取整)

状态标记

用二进制位表示多个布尔状态:

java 复制代码
public static void main(String[] args) {
    // 定义权限常量
    final int READ = 0b001;  // 二进制表示:001,十进制:1
    final int WRITE = 0b010; // 二进制表示:010,十进制:2
    final int EXECUTE = 0b100; // 二进制表示:100,十进制:4
    // 设置用户权限(读和写)
    int userPermission = READ | WRITE; // 按位或操作,结果为 0b011(有读和写权限)
    // 检查是否具有读权限 按位与:只有当两个对应的比特位都为 1 时,结果才为 1;否则为 0。  为1说明,只有相关权限
    if ((userPermission & READ) != 0) { // 按位与操作,检查是否有读权限
        System.out.println("允许读取");
    }

    // 如果需要检查其他权限,可以类似地添加条件
    if ((userPermission & WRITE) != 0) {
        System.out.println("允许写入");
    }

    if ((userPermission & EXECUTE) != 0) {
        System.out.println("允许执行");
    }
}

输出

java 复制代码
允许读取
允许写入

Process finished with exit code 0

数据压缩

存储多个小数字到一个整数中,如将4个4位(二进制占四位)数字打包成一个16位整数

java 复制代码
public static void main(String[] args) {
    // 定义变量 a, b, c, d
    int a = 3, b = 5, c = 10, d = 7;
    // 将 a 左移 12 位:将 a 移动到最高 4 位(第 12 到第 15 位)。 3 << 12 → 0011000000000000
    // 将 b 左移 8 位:将 b 移动到次高 4 位(第 8 到第 11 位)。 5 << 8 → 0000010100000000
    // 将 c 左移 4 位:将 c 移动到中间 4 位(第 4 到第 7 位)。 10 << 4 → 0000000010100000
    // d:直接放在最低 4 位(第 0 到第 3 位)。 7 → 0000000000000111
    // (a << 12) | (b << 8) | (c << 4) | d:使用按位或操作将所有部分组合成一个完整的 16 位整数
    int packed = (a << 12) | (b << 8) | (c << 4) | d;
    System.out.println("Packed value: " + packed);
    // 解包操作
    // packed >> 12:将 packed 右移 12 位,使最高 4 位移到最低位。
    // (packed >> 8) & 0b1111:用 0b1111 按位与,保留最低 4 位(0b表示这个数字是二进制)
    int unpackedA = (packed >> 12) & 0b1111;
    int unpackedB = (packed >> 8) & 0b1111;
    int unpackedC = (packed >> 4) & 0b1111;
    int unpackedD = packed & 0b1111;
    // 输出解包后的值
    System.out.println("Unpacked a: " + unpackedA);
    System.out.println("Unpacked b: " + unpackedB);
    System.out.println("Unpacked c: " + unpackedC);
    System.out.println("Unpacked d: " + unpackedD);
}

输出

java 复制代码
Packed value: 13735
Unpacked a: 3
Unpacked b: 5
Unpacked c: 10
Unpacked d: 7

Process finished with exit code 0

找唯一数

利用异或性质:a ^ a = 0a ^ 0 = a

注意下面数组中:只有4为单独出现,其他元素成对出现,假如有多个单独出现,则无法找出唯一数

java 复制代码
public static void main(String[] args) {
    // 定义数组
    int[] arr = {4, 1, 2, 1, 2, 2, 2};

    // 初始化 result 为 0
    int result = 0;

    // 遍历数组,对每个元素进行异或操作
    for (int num : arr) {
        result ^= num;
    }

    // 输出结果
    System.out.println(result);
}

计算过程如下:

  • 初始状态:

result = 0 (二进制:0000

  • 第一步:num = 4
    • result ^= 4
    • 0000 ^ 0100 = 0100
    • result = 4 (二进制:0100
  • 第二步:num = 1
    • result ^= 1
    • 0100 ^ 0001 = 0101
    • result = 5 (二进制:0101
  • 第三步:num = 2
    • result ^= 2
    • 0101 ^ 0010 = 0111
    • result = 7 (二进制:0111
  • 第四步:num = 1
    • result ^= 1
    • 0111 ^ 0001 = 0110
    • result = 6 (二进制:0110
  • 第五步:num = 2
    • result ^= 2
    • 0110 ^ 0010 = 0100
    • result = 4 (二进制:0100
  • 第六步:num = 2
    • result ^= 2
    • 0100 ^ 0010 = 0110
    • result = 6 (二进制:0110
  • 第七步:num = 2
    • result ^= 2
    • 0110 ^ 0010 = 0100
    • result = 4 (二进制:0100
  • 最终结果:

result = 4 (二进制:0100

相关推荐
Tracy-221 小时前
啥是Spring,有什么用,既然收费,如何免费创建SpringBoot项目,依赖下载不下来的解决方法,解决99%问题!
java·spring
未定义.2211 小时前
Java设计模式实战:策略模式在SimUDuck问题中的应用
java·设计模式·策略模式
码熔burning3 小时前
【NIO番外篇】之组件 Channel
java·nio·channel
东阳马生架构3 小时前
Sentinel源码—1.使用演示和简介
后端
卡尔曼的BD SLAMer4 小时前
问题 | 针对SSM(Spring + Spring MVC + MyBatis)框架的去Spring MVC强化版学习路线
java·spring·mvc·mybatis
zhuyasen4 小时前
首个与AI深度融合的Go开发框架sponge,解决Cursor/Trae等工具项目级开发痛点
后端·低代码·go
春生野草4 小时前
0413-多态、Object类方法、访问权限修饰符、装箱拆箱、128陷阱
java·开发语言
烁3474 小时前
每日一题(小白)暴力娱乐篇25
java·数据结构·算法·娱乐
烁3474 小时前
每日一题(小白)暴力娱乐篇26
java·开发语言·算法·娱乐
山有木兮丶丶4 小时前
spring boot大文件与多文件下载
spring boot·后端