位运算,相比普通的代码最大的优点就是其带来的高效性,也因此可以常在底层源码中看见它们的踪影。

本文就位运算常见的操作作一个总结,若您另有关于位运算巧妙的运用可以于底部留言区留言。

首先还是先来回顾下位操作的基础知识。(除非特别说明,否则以下都以 2 进制为例)

相关基础


1. 与运算

与运算符 "&" 是双目运算符。只有对应的两个二进位均为 1 时,结果位才为 1,否则为 0。例如:

9 & 5 = 00001001      & 00000101      = 00000001      = 1


2. 或运算

或运算符 "|" 是双目运算符。只要对应的两个二进位有一个为 1 时,结果位就为 1。例如:

9 | 5 = 00001001      | 00000101      = 00001101      = 13


3. 非运算

非运算符 "~" 为单目运算符。其功能是对参与运算的各二进位求反。例如:

~ 9 = ~ 00001001    =   11110110    =   -10


4. 异或运算

异或运算符 "^" 是双目运算符。其功能是对参与运算的二进位相异或,即当两二进位相异时,结果为 1,相同就为 0。例如:

9 ^ 5 = 00001001      ^ 00000101      = 00001100      = 12


5. 左移和右移

 例 1例 2x0110001110010101x << 40011000001010000x >> 4(逻辑右移)0000011000001001x >> 4(算术右移)0000011011111001

左移动就是向左移动 k 位,丢弃最高的 k 位,并在右端补 k 个 0,也就是常说的当前值乘以 2 的 k 次方。

右移动的原理也是相同的,右移 k 位就是当前数除以 2 的 k 次方。唯一不同的是分为逻辑右移和算术右移。

逻辑右移就是无符号移位,右移几位,就在左端补几个 0。

算术右移动是有符号移位,和逻辑右移不同的是,算术右移是在左端补 k 个最高有效位的值,如此看着有些奇特,但对有符号整数数据的运算非常有用。我们知道有符号的数,首位字节,是用来表示数字的正负(1 为负)。负数采用补码形式来存储,比如 - 26(11100110),算术右移 1 位之后 - 13(11110011)。如若不是补最高有效位的值 1 而是补作 0 的话,右移之后就变成正数了。


经典应用


1. i+(~i)=-1

i 取反再与 i 相加,相当于把所有二进制位设为 1,其十进制结果为 - 1。


2. 计算 n+1 与 n-1

-~n == n + 1,~n 为其取反,负号 '-' 再对其取反并加 1。

~-n == n - 1,思路就是找到最低位的第一个 1,对其取反并把该位后的所有位也取反,即01001000变为01000111


3. 取相反数

思路就是取反并加 1,也即~n + 1或者(n ^ -1) + 1


4. if(x == a) x = b; if(x == b) x = a;

利用 ^ 运算符的性质,即得x = a ^ b ^ x


5. n 倍数补全

当 n 为 2 的幂时,(x + n - 1) & ~(n - 1)会找到第一个大于 x 的数,且它正好是 n 的整数倍。


6. 求二进制中 1 的个数

/* Version 1 */int count_1_bits(int n){    int count = 0;      while (n)    {        count++;        n = n & (n - 1);    }      return count;}/* Version 2 */int count_1_bits(int n){     x = (x & 0x55555555) + ((x >> 1)  & 0x55555555);     x = (x & 0x33333333) + ((x >> 2)  & 0x33333333);     x = (x & 0x0f0f0f0f) + ((x >> 4)  & 0x0f0f0f0f);     x = (x & 0x00ff00ff) + ((x >> 8)  & 0x00ff00ff);     x = (x & 0x0000ffff) + ((x >> 16) & 0x0000ffff);       return x;}

关于第二个版本,分析如下:(摘自 Matrix67 - 位运算,并作稍微修改)

以十进制数 211 为例,其二进制为 11010011,

| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <— 原数+---+---+---+---+---+---+---+---+|  1 0  |  0 1  |  0 0  |  1 0  |   <— 第一次运算后+-------+-------+-------+-------+|    0 0 1 1    |    0 0 1 0    |   <— 第二次运算后+---------------+---------------+|        0 0 0 0 0 1 0 1        |   <— 第三次运算后,得数为 5+-------------------------------+

整个程序是一个分治的思想。第一次我们把每相邻的两位加起来,得到每两位里 1 的个数,比如前两位 10 就表示原数的前两位有 2 个 1。第二次我们继续两两相加,10+01=11,00+10=10,得到的结果是 00110010,它表示原数前 4 位有 3 个 1,末 4 位有 2 个 1。最后一次我们把 0011 和 0010 加起来,得到的就是整个二进制中 1 的个数。


7. 判断二进制中 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 为奇数

以下分析摘自 Matrix67 - 位运算,并作稍微修改,

以十进制数 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 的奇偶性。


8. 判断奇偶性

/* 判断是否是奇数 */bool is_odd(int n){    return (n & 1 == 1);}


9. 不用临时变量交换两个数

/* 此方法对 a 和 b 相等的情况不适用 */a ^= b;  b ^= a;  // 相当于 b = b ^ ( a ^ b );a ^= b; 


10. 取绝对值

/* 注意:以下的数字 31 是针对 int 大小为 32 而言 */int abs(int n){      return (n ^ (n >> 31)) - (n >> 31);  } 

其中n >> 31取得 n 的正负号。

  • 若 n 为正数,n >> 31的所有位等于 0,其值等于 0。表达式转化为n ^ 0 - 0,等于 n;

  • 若 n 为负数,n >> 31的所有位等于 1,其值等于 - 1。表达式转化为(n ^ -1) + 1,这很好理解,负数的相反数就是对其补码取反再加 1,(n ^ -1) + 1就是在做这样的事。


11. 取两数的较大值

/* 注意:以下的数字 31 是针对 int 大小为 32 而言 */int max(int a, int b){    return (b & ((a - b) >> 31)) | (a & (~(a - b) >> 31));}  

如果a >= b(a - b) >> 31为 0,否则为 - 1。


12. 判断符号是否相同

/* 若 x,y 都为 0,输出真;若只有一个为 0,不会报错但运行结果是错的,因为 0 没有正负之分 */bool is_same_sign(int x, int y){    return (x ^ y) >= 0;}  


13. 判断一个数是不是 2 的幂

bool is_power_of_two(int n){    return (n > 0) ? (n & (n - 1)) == 0 : false;}  

如果是 2 的幂,n - 1就是把 n 的二进制的最低的那个 1 取反为 0,并把后面的 0 全部取反为 1。


14. 取余 2 的幂次方

/* 其中 m 为 2 的幂次方,并对 m 取余 */int mod(int n, int m){    return n & (m - 1);  }


参考文献

  • Matrix67.

  • Bit Twiddling Hacks.