Bit Twiddling Hacks
位运算就是基于整数的二进制表示进行的运算。由于计算机内部就是以二进制来存储数据,位运算是相当快的。
运算符
基本的位运算共 6 种,分别为按位与、按位或、按位异或、按位取反、左移和右移。
运算 | 运算符 | 解释 |
---|---|---|
与 | & |
只有两个对应位都为 1 时才为 1 |
或 | | |
只要两个对应位中有一个 1 时就为 1 |
异或 | ^ |
只有两个对应位不同时才为 |
取反 | ~ |
0变1, 1变0 |
左移 | << |
各二进位全部左移若干位,高位丢弃,低位补0 |
右移 | >> |
各二进位全部右移若干位,对于无符号数,高位补0; 有符号数,有的补符号位 |
按位与 &
相同位的两个数字都为1,则为1;若有一个不为1,则为0。
示例:6&11
1 | 6 0 1 1 0 |
and运算通常用于二进制的取位操作。
小技巧:一个数 & 1的结果就是取二进制的最末位。这可以用来判断一个整数的奇偶,二进制的最末位为0表示该数为偶数,最末位为1表示该数为奇数。
按位或 |
相同位只要一个为1即为1
示例:6 | 11
1 | 0 1 1 0 |
or运算通常用于二进制特定位上的无条件赋值。
小技巧:一个数or 1的结果就是把二进制最末位强行变成1。如果需要把二进制最末位变成0,对这个数or 1之后再减一就可以了,其实际意义就是把这个数强行变成最接近的偶数。
按位异或 ^
两个位相同为0,相异为1
示例:6 ^ 11
1 | 0 1 1 0 |
^运算通常用于翻转指定位。
小技巧:将数 X=1010 1110 的低4位进行翻转,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行异或运算(X^Y=1010 0001)即可得到。
按位取反
not运算的定义是把内存中的0和1全部取反。
示例:~ 6
使用按位取反运算符,要知道几点:
1、内存中,一个int,4个字节,1字节8位。
2、有符号整数的按位取反情况略有偏差。
1 | 00000000 00000000 00000000 00000110 |
小技巧:可以用~使每个数的最低位为0,方法:a & ~1
左移位运算符 <<
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
示例:6 << 2
1 | 0 1 1 0 |
小技巧:若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
右移位运算符 >>
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
示例: 6 >> 2
1 | 0 1 1 0 |
小技巧:操作数每右移一位,相当于该数除以2。
负数的二进制表示
用字节的最高位表示:”1”表示”正”,”0”表示”负”
1、把补码“取反”(把二进制数的各位“1”换“0”,“0”换“1”。比如“101010”取反后为“010101”)
2、把取反后的二进制数“+1”
示例:-17 转码
1 | 17: 00000000 00000000 00000000 00010001 |
优先级
Go语言运算符优先级和结合性一览表
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | | | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高。
基本操作
- x & (- x):保留二进制下最后出现的1的位置,其余位置置0(即一个数中最大的2的n次幂的因数
- x & (x - 1):消除二进制下最后出现1的位置,其余保持不变
- x & 0 = 0
- (a ^ b) ^ c == a ^ (b ^ c)
- a = 0 ^ a = a ^ 0
- 对于任何数x,都有x ^ x=0,x ^ 0=x
- 自反性: a ^ b ^ b=a ^ 0=a
- 移除最后一个 1 : a = n & (n - 1)
- 获取最后一个 1: diff = (n & (n - 1)) ^ n, 跟x & (- x) 一样,都是保留二进制下最后出现的1的位置
高端操作
实现swap
1 | //普通操作 |
a = b的时候会出问题
1 | a ^= b --> a = a^b = 0 |
快速求数字二进制表示中 1 的个数
1 | // n & (n - 1):消除最低位的 1 |
判断奇偶数
只要根据数的最后一位是 0 还是 1 来决定即可,为 0 就是偶数,为 1 就是奇数。
1 | if(0 == (a & 1)) { |
判断符号是否相同
1 | bool isSameSign(int x, int y) { // 有 0 的情况例外 |
一个正数补码的符号位和一个负数补码的符号位肯定相反,经过异或运算后,最后符号位结果肯定是 1
(代表负数)。
移除最后一位 1
1 | byte n = 10; |
n & (n -1 )
主要作用:消除数字 n 的二进制表示中的最后一个 1
获取最后一个 1
两种方法
1 | (n & (n-1)) ^ n; |
Herbert : x & (x- 1) ^x == x & (-x) 这个等式是正确的吗?
ChatGpt:
我们可以分别证明等式的两边,最终证明它们相等。
左边:x & (x-1) ^ x
因为按位异或运算 ^ 的规则是:相同位上的数字不同则为 1,否则为 0,所以 (x-1) ^ x 的结果是把 x 的最低位上的 1 变成 0。因此 x & (x-1) 会把 x 的最低位上的 1 变成 0,其它位不变。也就是说,它把 x 中最低位上的 1 和它右边所有的 0 都清除掉了。这个操作等价于将 x 取整个数的二进制表示中,最低的一位 1 之后的所有位都清零。所以,左边的等式可以看成是:x 取整个数的二进制表示中,最低的一位 1 之后的所有位都清零。
右边:x & (-x)
-x 表示的是 x 的补码的相反数。在计算机中,一个数 x 的补码等于它的反码加 1。反码的规则是,正数的反码就是其原码,负数的反码就是其原码除符号位以外取反。因此-x 的补码就是(x 的原码除符号位以外取反加 1)。因此,-x 和 & 运算的结果是将 x 的值除最低位以外的所有位取反并加 1,最低位不变。也就是说,它把 x 中最低位上的 1 和它右边所有的 0 都清除掉了,其它位不变。这个操作等价于将 x 取整个数的二进制表示中,最低的一位 1 之后的所有位都清零。
因此,左边和右边的值是相等的,等式成立。
交换符号
将正数变成负数,负数变成正数
1 | int reversal(int a) { |
整数取反加1,正好变成其对应的负数(补码表示);负数取反加一,则变为其原码,即正数5.
求绝对值
整数的绝对值是其本身,负数的绝对值正好可以对其进行取反加一求得,即我们首先判断其符号位(整数右移 31 位得到 0,负数右移 31 位得到 -1,即 0xffffffff),然后根据符号进行相应的操作
1 | int abs(int a) { |
上面的操作可以进行优化,可以将 i == 0 的条件判断语句去掉。我们都知道符号位 i 只有两种情况,即 i = 0 为正,i = -1 为负。对于任何数与 0 异或都会保持不变,与 -1 即 0xffffffff 进行异或就相当于对此数进行取反,因此可以将上面三目元算符转换为((a^i)-i),即整数时 a 与 0 异或得到本身,再减去 0,负数时与 0xffffffff 异或将 a 进行取反,然后在加上 1,即减去 i(i =-1)
1 | int abs2(int a) { |
大小写字母转换
1 | 利用 或操作 和空格将英文字母转成小写 |
上述技巧能够产生奇特效果的原因在于字符类型的数据都是通过 ASCII 进行编码的,字符本身其实就是数字,而刚好这些字符对应的数字通过位运算符就可以得到正确结果。
用 O(1) 时间检测整数 n 是否是 2 的幂次
N如果是2的幂次,则N满足两个条件。
N >0
N的二进制表示中只有一个1
因为N的二进制表示中只有一个1,所以使用N & (N - 1)将N唯一的一个1消去,应该返回0。
1 | bool isPowerOfTwo(int n) { |
对 2 的非负整数次幂取模
1 | int modPowerOfTwo(int x, int mod) { |
表示集合
一个数的二进制表示可以看作是一个集合(0 表示不在集合中,1 表示在集合中)。比如集合 {1, 3, 4, 8}
,可以表示成 ((100011010)_2) 。而对应的位运算也就可以看作是对集合进行的操作。
操作 | 集合表示 | 位运算语句 |
---|---|---|
交集 | (a \cap b) | a & b |
并集 | (a \cup b) | a | b |
补集 | (\bar{a}) | ~a (全集为二进制都是 1) |
差集 | (a \setminus b) | a & (~b) |
对称差 | (a\triangle b) | a ^ b |
数组中,只有一个数出现一次,剩下都出现两次,找出出现一次的数
因为只有一个数恰好出现一个,剩下的都出现过两次,所以只要将所有的数异或起来,就可以得到唯一的那个数。(异或自反性)
乘/除 2 的非负整数次幂
1 | int mulPowerOfTwo(int n, int m) { // 计算 n*(2^m) |
warning : 我们平常写的除法是向 0 取整,而这里的右移是向下取整(注意这里的区别),即当数大于等于 0 时两种方法等价,当数小于 0 时会有区别,如: -1 / 2
的值为 0,而 -1 >> 1
的值为 -1
。
总结
1 | 1 |
以上,为最常见的用法,不带循环,全部O(1)。
leetcode example
Leetcode-260-single-number-iii