位运算
基础概念
位运算(Bitwise Operation)是直接对整数的二进制位进行操作的运算方式,由于操作底层二进制,执行效率极高,广泛用于系统编程、算法优化、数据压缩等场景。以下是位运算的核心概念、常用操作及应用:
- 二进制位:计算机中数据以二进制(0 和 1)存储,每个二进制数字称为 “位(bit)”。
- 操作对象:位运算的操作数是整数(正整数、负整数、0),运算时先转换为二进制补码形式(计算机存储整数的标准形式)。
- 运算特点:按位操作,即两个操作数的对应二进制位分别进行运算,结果组成新的二进制数。
常用位运算及规则
& 与运算 遇0则0
| 或运算 遇1则1
^ 异或 不进位加 相同为0 相异为1
~ 取反 1-0 0-1
>> 右移(逻辑位移) 补符号位 符号为1补1 符号为0补0
<< 左移(算数位移) 永远补0
>>> 无符号右移 永远补0
Kotlin 里的七种位运算符
and(bits) – 位与 (Java 的 &)
or(bits) – 位或 (Java 的 |)
inv() – 位非 (Java 的 ~)
xor(bits) – 位异或 (Java 的 ^)
shl(bits) – 有符号左移 (Java 的 <<)
shr(bits) – 有符号右移 (Java 的 >>)
ushr(bits) – 无符号右移 (Java 的 >>>)
| 符号 | 含义 | 详解 |
|---|---|---|
| & | 位与 | 两个比特位都为 1 时,结果才为 1,否则为 0 (位与操作满足交换律和结合律,甚至分配律) |
| | | 位或 | 两个比特位都为 0 时,结果才为 0,否则为 1 (位或操作满足交换律和结合律,甚至分配律) |
| ~ | 位非 | 即按位取反,1 变 0,0 变 1 |
| ^ | 异或 | 两个比特位相同时(都为 0 或都为 1)为 0,相异为 1(异或操作满足交换律和结合律,甚至分配律。任何整数和自己异或的结果为 0,任何整数与 0 异或值不变) |
| << | 左移 | 将所有的二进制位按值向←左移动若干位,左移操作与正负数无关,它只是傻傻地将所有位按值向左移动,高位舍弃,低位补 0 |
| >> | 右移 | 将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,而高位补 0 还是补 1 则跟被操作整数的正负相关,正数补 0 ,负数补 1 |
| >>> | 无符号右移 | 将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,高位补 0 ,也跟正负无关 |
1. 按位与(AND):&
- 规则:两个对应位都为 1 时,结果位为 1;否则为 0。 (
1 & 1 = 1,1 & 0 = 0,0 & 1 = 0,0 & 0 = 0) - 示例:
5 & 3→ 二进制101 & 011 = 001→ 结果为1。 - 用途:
- 提取指定位(如
x & 0b111提取 x 的低 3 位)。 - 判断奇偶(
x & 1 == 1为奇数,==0为偶数)。
- 提取指定位(如
2. 按位或(OR):|
- 规则:两个对应位有一个为 1 时,结果位为 1;否则为 0。 (
1 | 1 = 1,1 | 0 = 1,0 | 1 = 1,0 | 0 = 0) - 示例:
5 | 3→ 二进制101 | 011 = 111→ 结果为7。 - 用途:
- 设置指定位为 1(如
x | 0b100将 x 的第 3 位设为 1)。 - 合并标志位(如权限控制中合并多个权限)。
- 设置指定位为 1(如
3. 按位异或(XOR):^
- 规则:两个对应位不同时,结果位为 1;相同则为 0。 (
1 ^ 1 = 0,1 ^ 0 = 1,0 ^ 1 = 1,0 ^ 0 = 0) - 示例:
5 ^ 3→ 二进制101 ^ 011 = 110→ 结果为6。 - 特性:
- 自身异或为 0(
x ^ x = 0)。 - 与 0 异或不变(
x ^ 0 = x)。 - 交换律和结合律(
a ^ b ^ c = a ^ (b ^ c))。
- 自身异或为 0(
- 用途:
- 交换两个数(
a = a ^ b; b = a ^ b; a = a ^ b,无需临时变量)。 - 查找数组中唯一出现奇数次的数(异或所有元素,结果即为目标)。
- 交换两个数(
4. 按位取反(NOT):~
规则:将二进制位 0 变 1,1 变 0(包括符号位)。
示例: 对于 32 位整数,
~5→ 二进制~000...000101 = 111...111010→ 结果为-6(补码规则)。用途:
- 生成掩码(如
~0 << n生成高(32-n)位为 1 的掩码)。 - 计算相反数(
~x + 1 = -x,补码中负数是正数取反加 1)。
- 生成掩码(如
5. 左移(Shift Left):<<
- 规则:将二进制位整体左移 n 位,高位溢出丢弃,低位补 0。 (相当于
x * 2ⁿ,前提是不溢出) - 示例:
5 << 2→ 二进制101 << 2 = 10100→ 结果为20(5×2²=20)。 - 用途:快速计算乘以 2 的幂(比乘法运算高效)。
6. 右移(Shift Right):>>(算术右移)与 >>>(逻辑右移)
- 算术右移(>>): 二进制位整体右移 n 位,低位溢出丢弃,高位补符号位(正数补 0,负数补 1)。 (相当于
x ÷ 2ⁿ,向下取整) 示例:-8 >> 2→ 二进制11111000 >> 2 = 11111110→ 结果为-2。 - 逻辑右移(>>>): 二进制位整体右移 n 位,低位溢出丢弃,高位补 0(不考虑符号位,仅用于无符号数)。 示例:
-8 >>> 2(32 位)→ 结果为1073741822(高位补 0 后的值)。 - 用途:快速计算除以 2 的幂、提取高位等。
复合位运算(赋值操作)
位运算可与赋值结合,简化代码:
x &= y→ 等价于x = x & y(按位与后赋值)x |= y→ 等价于x = x | y(按位或后赋值)x ^= y→ 等价于x = x ^ y(按位异或后赋值)x <<= n→ 等价于x = x << n(左移后赋值)x >>= n→ 等价于x = x >> n(右移后赋值)
典型应用场景
- 权限控制:用二进制位表示不同权限(如
0b101表示权限 1 和 3),通过|合并权限、&检查权限。 例:hasPermission = (userPerm & targetPerm) == targetPerm。 - 算法优化:
- 求两数平均值:
(a + b) >> 1(避免溢出可用a + ((b - a) >> 1))。 - 清除最低位的 1:
x & (x - 1)(如6 (110) & 5 (101) = 4 (100))。 - 判断 2 的幂:
x & (x - 1) == 0(2 的幂的二进制只有一个 1)。
- 求两数平均值:
- 数据压缩与加密:位运算常用于哈希算法(如 MD5)、对称加密(如 AES)中的底层变换,高效处理二进制数据。
- 硬件编程:嵌入式系统中,通过位运算操作寄存器的特定位(如控制 GPIO 引脚高低电平)。
注意事项
- 符号位处理:负数以补码存储,位运算会操作符号位(如
~会翻转符号位),需注意结果的正负。 - 溢出问题:左移可能导致整数溢出(超出类型表示范围),结果可能不符合预期(如 Java 中
1 << 31是-2147483648)。 - 可读性权衡:位运算效率高,但过度使用会降低代码可读性,需在性能与可读性间平衡。
位运算的核心价值在于直接操作底层二进制,这使得它在需要高性能或直接与硬件交互的场景中不可替代。掌握位运算规则和常见技巧,能显著提升代码效率和解决复杂问题的能力。
位运算常用操作
基本
a & a = a
a | a = a
a ^ a = 0
a & ~a == 0;
a | ~a == -1;
a ^ ~a == -1;
a & 0 = 0
a | 0 = a
a ^ 0 = a
a & -1 = a
a | -1 = -1
a ^ -1 = -(a+1)
a | ( a & b ) = a
a & ( a | b ) = a
// 当且仅当 a 是偶数(即 a 能被 2 整除,数学表示为 (a = $2k$, k \in \mathbb{Z}\))时,等式成立
a | 1 = a + 1
// 如果a是奇数,(a + 1)如果a是偶数
a ^ 1 = (a - 1)
// 删除整数 a 二进制最后一个 1
// a-1 会翻转 a 最后一个 1 及右侧所有位,与 a 按位与后仅保留左侧不变的位
a & (a-1)
### 统计二进制中 1 的个数(汉明重量)
原理:每执行一次 a & (a - 1),就会删除一个 1,直到 a 变为 0(所有 1 都被删除),执行次数即为 1 的个数。
示例(统计 a=13 二进制中 1 的个数,13=1101,共 3 个 1):
1. a = 13 (1101) → a & (a-1) = 12 (1100) → 次数1;
2. a = 12 (1100) → a & (a-1) = 8 (1000) → 次数2;
3. a = 8 (1000) → a & (a-1) = 0 (0000) → 次数3;
a=0,停止,结果为3。
// 判断一个数是否为 2 的幂
原理:2 的幂(如 2、4、8、16)的二进制有且只有一个 1(如 8=1000)。若 a 是 2 的幂,则 a & (a - 1) = 0(唯一的 1 被删除后变为 0);反之则不为 0。
示例:
a=8(2³):8 & 7 = 0 → 是 2 的幂;
a=9(1001):9 & 8 = 8 ≠ 0 → 不是 2 的幂。
2.2
a & (-a) = a(保留自身)
示例:8 & (-8) = 8 → 是 2 的幂;6 & (-6) = 2 ≠ 6 → 不是 2 的幂。
// 清除二进制末尾的连续 0
结合其他操作,可先通过 a & -a 找到最后一个 1 的位置(如 a=12=1100,a&-a=4=100),再用 a & (a - 1) 删除该 1,间接清除末尾的连续 0。
// 负数
-a = ~a + 1
// 仅保留最后一个1;
a & (-a)
a & -a 或者 a & (~a + 1) 获取最右侧1表示的值
比如:
4 二进制是 100 4 & -4 等于0 4最右侧的1后面的值就是4的本身100
5 二进制是 101 5 & -5 等于1 5最右侧的1后面的值就是1, 如果 5 - (5 & -5) 就相当于直接去掉最右侧的1
6 二进制是 110 6 & -6 等于2 6最右侧的1后面的值就是2, 如果 6 - (6 & -6) 就相当于直接去掉最右侧的1
// 比较两值是否相等
a ^ b == 0
// i+1位 置1
a |= 1<<i
// i+1位 置0
a &=~(1<<i)
// 取出i+1位(联系第5点)
a & (1<<i)
// 在对应i+1位,插入b的对应位;
a |=1<<i; (a的bit位置1)
a & (b & 1<<i) (与b的bit位相与)
// 得到全1
~0
// 保留最后i-1位;
a & ((1<<i)-1)
// 清零最后i-1位;
a & ~((1<<i)-1)
// 判断最高位是否为1
a<0
// 得到最高位的1;
a = a |(a>>1);
a = a |(a>>2);
a = a |(a>>4);
a = a |(a>>8);
a = a |(a>>16);
return (a+1)>>1;
判断奇偶
可以很简单归纳出,只需判断一个正整数的二进制补码最后一位是0是1,是0为偶数,是1为奇数
a & 1 = 0 或 1 a为奇数返回1、a为偶数返回0
// 判断奇偶(取出最后一位)
a & 1 等价于 a % 2(结果等于,位运算效率高)
public void isOddOrEven(int n){
if ((n & 1) == 1) { //n是奇数
System.out.println("Odd");
}else { //n是偶数
System.out.println("Even");
}
}
a、b两个数交换
a = a ^ b
b = a ^ b
a = a ^ b
方式一:通过临时变了存放交换 效率最高
int temp = a;
a = b;
b = temp;
方式二:通过加减计算交换
a = a + b;
b = a - b;
a = a - b;
方式三:通过异或运算交换
a = a ^ b;
b = a ^ b;
a = a ^ b;
a ^= b;
b ^= a;
a ^= b;
方式四:非常不推荐,代码的简洁性是很重要的,但是也许面试题会考到
a = (a + b) - (b = a);
//用到的知识点:
//赋值运算符在连续赋值的时候会使用初始值代替中间变量
方式四:不推荐,理由同上
a = (a ^ b) ^ (b = a);
首先需要知道关于异或运算的一些性质:
异或满足交换律和结合律
x ^ x == 0
x ^ 0 == x
方式一是最常见的,可读性高,但是需要在内存中存放临时变量,但是对于现在来说,需要的内存空间很小,而且存放临时变量的内存很快就会释放,不存在问题。
方式二有缺陷,当两个数相加之后,可能其结果超出了变量类型能表达的最大范围,这个时候结果就会出问题,不推荐使用。
方式三效率是最高的,但是可读性不是很好。
在程序中我们应该尽可能的使用方式一,提高程序的可读性。但是需要了解方式三,以及方式三的原理。
变换符号,正变负,负变正
只需对待操作数应用取反操作后再加 1 即可
public void negate(){
int a = -10, b = 10;
System.out.println(~a + 1);//10
System.out.println(~b + 1);//-10
}
求绝对值
对于负数可以通过上面变换符号的操作得到绝对值, 正数直接返回即可,因此我们要先判断符号位来得知当前数的正负。
public int abs(int a){
int i = a >> 31;//得到符号位,0 为正数,-1 为负数
return i == 0 ? a : (~a + 1);//符号位为 0 直接返回,否则返回 ~a + 1
}
或者,n>>31 取得n的符号,若n为正数,n>>31等于0,若n为负数,n>>31等于-1
若n为正数 n^0-0 数不变,若n为负数n^-1 需要计算n和-1的补码,异或后再取补码,
结果n变号并且绝对值减1,再减去-1就是绝对值
public int abs(int a){
return (a ^ (a >> 31)) - (a >> 31);
}
判断一个数num是不是 2 的幂
如果是2的幂,n一定是100... (也就是二进制位里只有一个是1,且是首位,其余全是0), n-1就是011.... 所以做与运算结果为 0
判断一个数num是不是 4 的幂
n > 0 && n & (n-1) == 0 && n % 3 == 1
int 型最大值是什么?
System.out.println((1 << 31) - 1);// 2147483647, 注意运算符优先级,括号不可省略
System.out.println(~(1 << 31));// 2147483647
int 型最小值是什么?
System.out.println(1 << 31);
System.out.println(1 << -1);
long 型最大值是什么?
System.out.println(((long)1 << 127) - 1);
整数n乘以2是多少?
n << 1;
整数n除以2是多少?(负奇数的运算不可用)
n >> 1;
乘以2的n次方,例如计算10 * 8(8是2的3次方)
System.out.println(10<<3);
除以2的n次方,例如计算16 / 8(8是2的3次方)
System.out.println(16>>3);
取两个数的最大值(某些机器上,效率比a>b ? a:b高)
System.out.println(b&((a-b)>>31) | a&(~(a-b)>>31));
取两个数的最小值(某些机器上,效率比a>b ? b:a高)
System.out.println(a&((a-b)>>31) | b&(~(a-b)>>31));
判断符号是否相同(true 表示 x和y有相同的符号, false表示x,y有相反的符号。)
System.out.println((a ^ b) > 0);
计算2的n次方 n > 0
System.out.println(2<<(n-1));
求两个整数的平均值
System.out.println((a+b) >> 1);
从低位到高位,取n的第m位
int m = 2;
System.out.println((n >> (m-1)) & 1);
从低位到高位.将n的第m位置为1,将1左移m-1位找到第m位,
//得000...1...000,n 再和这个数做或运算
System.out.println(n | (1<<(m-1)));
从低位到高位,将n的第m位置为0,将1左移m-1位找到第m位,
//取反后变成111...0...1111,n再和这个数做与运算
System.out.println(n & ~(0<<(m-1)));
清零
如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
& 取一个数的指定位
比如取数 X=1010 1110 的低4位,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位与运算(X&Y=0000 1110)即可得到X的指定位。
| 常用来对一个数据的某些位设置为1
比如将数 X=1010 1110 的低4位设置为1,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位或运算(X|Y=1010 1111)即可得到。
^ 翻转指定位
比如将数 X=1010 1110 的低4位进行翻转,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行异或运算(X^Y=1010 0001)即可得到。
~ 使一个数的最低位为零
使a的最低位为0,可以表示为:a & ~1。~1的值为 1111 1111 1111 1110,再按"与"运算,最低位一定为0。因为“ ~”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。
资料
https://wdxtub.com/interview/14520595848890.html
https://juejin.im/post/5dc2cc0b6fb9a04a916d0ba0