【注】Stanford的计算机课程中对位运算进行了非常详细的总结,我这里面很多都是参考其中: https://graphics.stanford.edu/~seander/bithacks.html
一、位运算基础
与(&)运算
- 一个数n与0进行与运算,值为0,
n & 0 = 0
。 - 一个数n与
-1
进行与运算,值为n,n & -1 = n
。 - 一个数n与自己进行与运算,值为n,
n & n = n
。
或(|)运算
- 一个数n与0进行或运算,值为n,
n | 0 = n
。 - 一个数n与
-1
进行或运算,值为-1,n | -1 = -1
。
非(~)运算
- 对二进制的每一位都按位取反。
- 对于数n,
n + (~n) = -1
。
异或(^)运算
运算的二进位结果,相异为1,相同为0。
- 一个数n与0异或,值为n,
n ^ 0 = n
。 - 一个数n与
-1
异或,值为~n
,n ^ -1 = ~n
。 - 一个数n与自己异或,值为0,
n ^ n = 0
。
左移(<<)和右移(>>)运算
- 向左进行移位操作,高位丢弃,低位补 0
- 向右进行移位操作,对无符号数,高位补 0,对于有符号数,高位补符号位
二、算法对n + (~n) = -1应用
设整数n类型为int_8,值为3,则3 + (~3) = 0000 0011 + 1111 1100 = 1111 1111 = -1
,所以引出非运算的基础公式n + (~n) = -1
,也可以将n ^ -1 = ~n
带入。
2.1 位运算实现n+1与n-1
对n + (~n) = -1
进行等式变换可得:
int n;
~n = -(n + 1);
n + 1 = -~n;
n - 1 = ~-n; // 假设n = -n,可推出此等式
2.2 取相反数
一个数的相反数等于其按位取反后再加1,对上等式变换推出:
int n;
-n = ~n + 1;
2.3 取绝对值
这一块内容也用到了n ^ 0 = n
和n ^ -1 = ~n
:
- 若n为非负数,则
n >> 31 = 0
,所以abs = n ^ 0 - 0 = n
。 - 若n为负数,则
n >> 31 = -1
,所以abs = n ^ (-1) + 1 = ~n + 1 = -1 - n + 1 = -n
。
int abs, n;
abs = (n ^ (n >> 31)) - (n >> 31)
三、算法对n ^ n = 0和n ^ 0 = n应用
3.1 交换两个数的值
不需要第三个临时变量,交换两个数的值
int a, b;
a ^= b; // a = a ^ b;
b ^= a; // b = b ^ a = b ^ a ^ b = (b ^ b) ^ a = 0 ^ a = a
a ^= b; // a = a ^ b = a ^ a ^ b = 0 ^ b = b
3.2 代替特定的条件赋值
如果x = a
,则a ^ b ^ x = 0 ^ b
;如果x = b
,则a ^ b ^ x = 0 ^ a
;
所以下列代码可等价于:x = a ^ b ^ x
。
int a, b, x;
if(x == a)
x = b;
else if(x == b)
x = a;
// 上面代码等价于
x = a ^ b ^ x
四、算法对 x&(x-1)应用
x&(x-1)
可以消除数字x二进制表示的最后一个1,如:
int x = 0xf6;
printf("%x\n", x); //0b11110110
printf("%x\n", x&(x-1)); //0b11110100
4.1 判断一个正数是不是2的次幂
如果一个正数是2的次幂,则这个数的二进制表示中只含有一个1。
int x;
if(x&(x-1)){
//x至少含有两个1,所以不是2的次幂
}
4.2 计算一个数的二进制含有多少个1
x中的最后一个1可以通过操作x = x&(x-1)
循环消去,当最后x值为0时,便可以求出二进制中1的个数。
int x, total;
while(x > 0){
x = x&(x-1);
total++;
}
五、算法对”2的次方“应用
5.1 整数对2的乘/除法
整数n向右移一位,相当于将n除以 2;数n向左移一位,相当于将n乘以 2
int n = 2;
n >> 1; // 1
n << 1; // 4
5.2 n对“2的次方”取余
m是2的次方,则其二进制数只有一个1,如 4 => 0100
,8 => 1000
。m-1
之后,原本m二进制的1变成0,原本1后面的0全变成1,如 4-1 = 3 => 0011
,8-1 = 7 => 0111
。
2
0
=
1
2^0 = 1
20=1
2
1
=
2
2^1 = 2
21=2
2
2
=
4
2^2 = 4
22=4
2
3
=
8
2^3 = 8
23=8
2
4
=
16
2^4 = 16
24=16
2
5
=
32
2^5 = 32
25=32
.
.
.
...
...
可以看出 2 q + 1 2^{q+1} 2q+1 永远都是 2 q 2^q 2q的整数倍,而 2 q 2^q 2q比 2 q − 1 + 2 q − 2 + . . . + 2 0 2^{q-1} + 2^{q-2} + ... + 2^0 2q−1+2q−2+...+20的和还要大。
假设
m
=
2
q
m = 2^q
m=2q,q为正整数。n的二进制数中,第[n的最高位, q]
位的和是m的整数倍,而第[q-1, 0]
位的和是n/m
的余数,也就是说将n的二进制数的第[q-1, 0]
位截取,即可得到n/m
的余数。
m
=
2
q
,
m
−
1
=
2
q
−
1
+
2
q
−
2
+
.
.
.
+
2
0
=
>
00011...1
m = 2^q,m - 1 =2^{q-1} + 2^{q-2} + ... + 2^0 => 00011...1
m=2q,m−1=2q−1+2q−2+...+20=>00011...1([q-1, 0]
位全为1),所以n & (m - 1)
的值为n/m
的余数。
int mod, n, m; // m是2的次方,如4,8等
mod = n & (m - 1);
5.3 将n以“2的次方”倍数最小补全
有n和m两数,m为2的次方,找到大于等于n且正好是m整数倍的最小数。看不懂以下内容就先阅读《5.2 n对“2的次方”取余》。
假设
m
=
2
q
m = 2^q
m=2q,则m的倍数的二进制数中,第q位为1,其余位全为0;(m - 1)
的二进制数中[q-1, 0]
位全为1,其余位全为0。
所以有两种情况:
- 如果n的二进制数中
[q-1, 0]
位全为0,则n就是m整数倍的最小数; - 如果n的二进制数中
[q-1, 0]
位不全为0,在第q位上加1,所得结果就是m整数倍的最小数。
现给n加上一个[q-1, 0]
位全为1,其余位全为0的数(m-1
就是这个数),并将所得结果的[q-1, 0]
位全部置为0(结果与~(m - 1)
相与即可),便可满足这两种情况。
int min, n, m;
min = (n + m - 1) & ~(m - 1);
【实战】内存对齐运算
函数的作用是将内存值n进行内存对齐,m < 1 || m > 8 || m&(m-1) != 0
表示对齐值m只能是4字节、8字节或16字节。
int64 MemAlign(n int64, m int64){
if m < 1 || m > 16 || r&(r-1) != 0 {
// 抛出error
return 0;
}
return (n + m - 1) & ~(m - 1)
}
六、综合技巧应用
6.1 判断一个整数的奇偶性
对于数n,若n的二进制数的最低位为0,n为偶数,否则为奇数。
if(0 == (n & 1)){
// 偶数
} else {
// 奇数
}
6.2 判断一个数二进制中1的奇偶性
首先可以用《4.2 计算一个数的二进制含有多少个1》的方法,这里介绍一种新的方法。
以十进制数1314520为例,其二进制为0001 0100 0000 1110 1101 1000
。
第一次异或操作的结果如下:
0001 0100 0000 1110 1101 1000
^ 0000 1010 0000 0111 0110 1100
= 0001 1110 0000 1001 1011 0100
得到的结果是一个新的二进制数,其中右起第i位上的数表示原数中第i和i+1位上有奇数个1还是偶数个1。比如,最右边那个0表示原数末两位有偶数个1,右起第3位上的1就表示原数的这个位置和前一个位置中有奇数个1。
对这个数进行第二次异或的结果如下:
0001 1110 0000 1001 1011 0100
^ 0000 0111 1000 0010 0110 1101
= 0001 1001 1000 1011 1101 1001
结果里的每个1表示原数的该位置及其前面三个位置中共有奇数个1,每个0就表示原数对应的四个位置上共偶数个1。
一直做到第五次异或结束后,得到的二进制数的最末位就表示整个32位数里1的奇偶性
x = x ^ (x >> 1);
x = x ^ (x >> 2);
x = x ^ (x >> 4);
x = x ^ (x >> 8);
x = x ^ (x >> 16);
cout << (x & 1) << endl; // 输出 1 为奇数
6.3 判断两个数的符号是否相同
如果两个数x和y符号相同,符号位异或结果为0;符号不同,符号位异或结果为1。
0和正数的符号位都是0,所以0 ^ 正数 > 0
。两个相等的值异或,值为零。
bool f = (x ^ y) < 0; // f = true, x和y符号不相同
if(f){
// x和y符号不相同
}
【实战】有符号加法溢出判断
两个正数相加可能会产生正溢出,正溢出结果会变为负数;两个负数相加可能会产生负溢出,负溢出结果会变为正数。所以利用好符号位,就可以判断值是否溢出。
int x, y, s;
s = x + y;
bool f = ((s^x) & (s^y)) < 0
if(f){
// overflow
}
无符号加法溢出判断虽然与这一块内容无关,但也提一下,详细介绍需阅读无符号数加法。记x+y
是两个数的和,2^w
是最高权重,如果x,y的和数据溢出,则和模(x+y) mod 2^w
比x,y中的任何一个值都小。
6.4 取两数的较 大/小 值
如果a >= b
,则a - b >= 0
且~(a - b) < 0
,所以((a - b) >> 31) = 0
且(~(a - b) >> 31) = -1
。
如果a < b
,则a - b < 0
且~(a - b) >= 0
,所以((a - b) >> 31) = -1
且(~(a - b) >> 31) = 0
。
int max(int a, int b){
return (b & ((a - b) >> 31)) | (a & (~(a - b) >> 31));
}
int min(int a, int b){
return (a & ((a - b) >> 31)) | (b & (~(a - b) >> 31)
}
6.5 判断一个数的二进制有效位数是否超出
判断一个数x的二进制是否超过b位:
- 如果x是无符号数,采用
x >> b == 0
即可判断; - 若x是有符号数,则不能这样,因为负数的符号位为1。设b = 12,则x的有效取值范围为
[1111 1000 0000 0000, 0000 0111 1111 1111]
,其中第12位是符号位,往上的高位与符号位保持一致。如果将x>>12
,则所得结果为0或-1;如果在x>>12
之前,给x的第12位加1,然后再将x>>12
,则所得结果为0。
#define LOONGF_U_OK(x, b) (((x) >> (b)) == 0)
#define LOONGF_S_OK(x, b) ((((x) + (1 << (b-1))) >> (b)) == 0)
// 判断x的二进制数是否超过12位
LOONGF_U_OK(x, 12)
LOONGF_S_OK(x, 12)