本文介绍了位运算的基础概念及其应用。位运算是直接对二进制位进行操作的高效运算方式,包括按位与(&)、或(|)、异或(^)、取反(~)、左移(<<)和右移(>>)。文章通过具体例子说明了位运算在判断奇偶、状态标记、数据压缩、快速计算以及找唯一数等场景中的实际用途。例如,利用位运算可以实现无需临时变量的数值交换,或者将多个小数字打包存储以节省空间。这些技巧不仅提升了程序性能,还在低级操作中发挥了重要作用。
位运算是计算机科学中一种直接对整数在内存中的二进制位进行操作的运算方式。它通过按位处理数据,可以高效地实现某些逻辑和数学操作。
常见的位运算符包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)。这些运算符可以直接操作二进制位,因此在需要高性能和低级操作的场景中非常有用。
计算机中所有数据以二进制形式存储(0和1),例如:
-8 的32位补码计算过程:
- 8的二进制:0000 0000 0000 0000 0000 0000 0000 1000
-
- 取反后:1111 1111 1111 1111 1111 1111 1111 0111
-
- 加1后:1111 1111 1111 1111 1111 1111 1111 1000
-
【规则】
两位都为1时结果为1,否则为0。
【例子】
计算5 & 3。
作用一:判断奇偶:x & 1 → 0是偶数,1是奇数
作用二:清零某些位:x & 0b11111011 可以清除第3位
注意:ob作为开头,用来表示该数字为二进制表示
- public static void main(String[] args) {
- int x = 6;
- x = x & 0b11111011;
- // 输出 2
- System.out.println(x);
- }
-
【规则】
两位中有一个为1时结果为1。
【例子】
5 | 3 -> 101 | 011 = 111(十进制7)
设置某些位为1:如x | 001000 可以设置第4位变为1。
【规则】
两位不同时结果为1,否则为0。
【例子】
5 ^ 3 -> 101 ^ 011 = 110(十进制6)
- # 假设 x = 7,二进制为 0111
- x ^ x = 0111 (7)
- ^ 0111 (7)
- ----
- = 0000 (0)
-
- # 假设 x = 7,二进制为 0111
- x ^ 0 = 0111 (7)
- ^ 0000 (0)
- ----
- = 0111 (7)
-
交换两个数:a ^= b; b ^= a; a ^= b;(无需临时变量)。
假设 a = 5 → 0101,b = 3 → 0011
步骤1:a ^= b
- a = a ^ b
- = 0101 (5)
- ^ 0011 (3)
- ----
- = 0110 (6)
- 此时 a = 6,b = 3。
-
步骤2:b ^= a
- b = b ^ a
- = 0011 (3)
- ^ 0110 (6)
- ----
- = 0101 (5)
- 此时 a = 6,b = 5。
-
步骤3:a ^= b
- a = a ^ b
- = 0110 (6)
- ^ 0101 (5)
- ----
- = 0011 (3)
- 此时 a = 3,b = 5,两个数的值已经交换。
-
在 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);
- }
-
经过实测使用位运算进行交换,效率更高
- 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");
- }
-
输出
- 位运算交换,时间:1ms
- 常规交换,时间:34ms
-
- Process finished with exit code 0
-
【规则】
所有位取反(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)。
【例子】
【规则】
所有位向右移动,左侧补0。
【例子】
-8 >>> 1 → 在32位中结果为 2147483644(不再保留符号)。
步骤1:将 -8 表示为32位二进制
步骤2:无符号右移 >>> 1
- 1111 1111 1111 1111 1111 1111 1111 1000
-
- 0111 1111 1111 1111 1111 1111 1111 1100
-
注意:最左侧补0,而不是补符号位。
步骤3:将结果转换为十进制
- 0111 1111 1111 1111 1111 1111 1111 1100
-
最高位是0,所以是正数。
- 2^30 + 2^29 + 2^28 + ... + 2^2
- = 1073741824 + 536870912 + 268435456 + ... + 4
- = 2147483644
-
- 所以结果是 2147483644
-
- 1111 1111 1111 1111 1111 1111 1111 1100
- 转换为十进制:-4。
-
位运算的速度通常比传统的算术运算快,因为它直接操作二进制位,在如下场景可以使用位运算进行加速:
用二进制位表示多个布尔状态:
- 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("允许执行");
- }
- }
-
输出
- 允许读取
- 允许写入
-
- Process finished with exit code 0
-
存储多个小数字到一个整数中,如将4个4位(二进制占四位)数字打包成一个16位整数
- 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);
- }
-
输出
- Packed value: 13735
- Unpacked a: 3
- Unpacked b: 5
- Unpacked c: 10
- Unpacked d: 7
-
- Process finished with exit code 0
-
利用异或性质:a ^ a = 0,a ^ 0 = a。
注意下面数组中:只有4为单独出现,其他元素成对出现,假如有多个单独出现,则无法找出唯一数
- 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)
result = 4 (二进制:0100)