🎯 本文介绍了位运算的基础概念及其应用。位运算是直接对二进制位进行操作的高效运算方式,包括按位与(&)、或(|)、异或(^)、取反(~)、左移(<<)和右移(>>)。文章通过具体例子说明了位运算在判断奇偶、状态标记、数据压缩、快速计算以及找唯一数等场景中的实际用途。例如,利用位运算可以实现无需临时变量的数值交换,或者将多个小数字打包存储以节省空间。这些技巧不仅提升了程序性能,还在低级操作中发挥了重要作用。
位运算是什么?
位运算是计算机科学中一种直接对整数在内存中的二进制位进行操作的运算方式。它通过按位处理数据,可以高效地实现某些逻辑和数学操作。
常见的位运算符包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)。这些运算符可以直接操作二进制位,因此在需要高性能和低级操作的场景中非常有用。
二进制是什么?
计算机中所有数据以二进制形式存储(0和1),例如:
- 十进制
5
→ 二进制101
- 十进制
3
→ 二进制011
- 十进制
-8
→ 二进制(补码)1111 1111 1111 1111 1111 1111 1111 1000
(以32位为例)
-8
的32位补码计算过程:
-
先写出
8
的二进制:Plain8的二进制:0000 0000 0000 0000 0000 0000 0000 1000
-
取反(按位取反):
Plain取反后:1111 1111 1111 1111 1111 1111 1111 0111
-
加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
。- 当
x
与0b11111011
进行&
操作时:- 第3位会被强制清零(因为
0 & 任何数 = 0
)。 - 其他位保持不变(因为
1 & 任何数 = 任何数
)。
- 第3位会被强制清零(因为
注意: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;
(无需临时变量)。
a ^= b
:将a
和b
的异或结果赋值给a
。b ^= a
:将b
和新的a
异或,结果赋值给b
(此时b
的值已经是原始的a
)。a ^= b
:将a
和新的b
异或,结果赋值给a
(此时a
的值已经是原始的b
)。
假设 a = 5 → 0101
,b = 3 → 0011
步骤1 :a ^= b
Plain
a = a ^ b
= 0101 (5)
^ 0011 (3)
----
= 0110 (6)
此时 a = 6,b = 3。
步骤2 :b ^= a
Plain
b = b ^ a
= 0011 (3)
^ 0110 (6)
----
= 0101 (5)
此时 a = 6,b = 5。
步骤3 :a ^= 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 >> 1
→1000
→0100
(十进制4)-8 >> 1
→11111111 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。
-
原始值:
Plain1111 1111 1111 1111 1111 1111 1111 1000
-
右移1位:
Plain0111 1111 1111 1111 1111 1111 1111 1100
- 注意:最左侧补0,而不是补符号位。
步骤3:将结果转换为十进制
-
右移后的二进制:
Plain0111 1111 1111 1111 1111 1111 1111 1100
-
转换为十进制:
-
最高位是0,所以是正数。
Plain2^30 + 2^29 + 2^28 + ... + 2^2 = 1073741824 + 536870912 + 268435456 + ... + 4 = 2147483644 所以结果是 2147483644
-
-
如果是带符号右移
-8 >> 1
:右移时,左侧补符号位(负数补1)。结果:Plain1111 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 = 0
,a ^ 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
)