2021.05.07更新:添加异或运算的性质
2020.03.23更新:添加位运算符~
的应用
2020.02.24更新:添加目录、调整格式。
2020.01.04更新:反码的缺点和使用,以及补码的优点。
一、前言
之前大一上课的时候,老师说位运算不考,当时就没有认真去学习。后来学算法的时候,发现大佬的代码中总会掺杂着一些位运算操作,为了理解那些代码,前前后后也看了几次位运算操作,但是没有很好地记录下来。现在又遇到了,借此机会写个总结。
由于位运算都是直接对整数在内存中的二进制数进行操作的,所以位运算的效率会相对比较高。
二、数的二进制表示
二进制数最高位为符号位,0表示正,1表示负(一般我们都不写出来)。所以C语言中的int(32位整数)的范围是
−
2
31
-2^{31}
−231到
2
31
−
1
2^{31}-1
231−1,第32位用来做符号位了。
正数的二进制表示即为它的原码,负数的二进制表示为它的补码。
1. 原码
把一个整数的绝对值转换为二进制数,再加上符号位就是该整数的原码。例如:
+9的原码为0 0001001
(即为+9的二进制表示)
-9的原码为1 0001001
2. 反码
正数的反码与它的原码相同,负数的反码符号位不变,数值部分为原码的按位取反。例如:
+9的反码为0 0001001
-9的反码为1 1110110
由于反码0的表示不唯一、表示数的范围比补码少一个最小负数、运算时必须考虑循环进位,因此反码在计算机中很少被使用,有时用作数码变换的中间表示形式。
3. 补码
正数的补码与它的原码相同,负数的补码为它的反码+1。例如:
+9的补码为0 0001001
-9的补码为1 1110111
(即为-9的二进制表示)
因为补码0的表示是唯一的,于是减少了+0和-0之间的转换;少占用了一个编码表示,使得补码能比原码多表示一个最小负数。
三、运算符
1. 按位与 &
对两个等长的(不够在前面用0补)二进制数进行按位与运算,如果对应位都为1,则该位为1,否则该位为0。
例如:求9&3,将它们转换为二进制数(以下都是先这样操作)
1001
0011
-----
0001
应用:
-
通常我们可以用n&1来判断n是奇数还是偶数,因为奇数的二进制最后一位都是1。
#include <iostream> using namespace std; int main() { for (int i = 0; i < 10; i++) if (i & 1) cout << i << " "; return 0; }
输出结果
1 3 5 7 9 -
用
n&(-n)
求非负数在二进制表示下最低位1以及它后面的0构成的数值。(这一求解应用在树状数组中) -
对于任意整数 x x x,令 x = x & ( x − 1 ) x=x \&(x-1) x=x&(x−1),该运算将 x x x 的二进制表示的最后一个 1 1 1 变成 0 0 0。于是,我们可以利用该性质快速求一个数二进制位有多少个1。
#include <iostream> using namespace std; int main() { int x, i; cin >> x; for (i = 0; x != 0; i++) x &= (x - 1); cout << i << endl; return 0; }
2. 按位或 |
对两个等长的(不够在前面用0补)二进制数进行按位或运算,如果对应位都为0,则该位为0,否则该位为1。
例如:求9|3
1001
0011
-----
1011
3. 按位取反 ~
~a表示对a的二进制表示的每一位进行取反,即0变为1,1变为0。例如:求~9
1001
-----
0110
应用:
用于求相反数,公式为:~n + 1
#include <stdio.h>
int main ()
{
int n = 10;
printf("%d ", ~n + 1);
return 0;
}
输出结果
-10
4. 按位异或 ^
对等长二进制模式按位或二进制数的每一位执行逻辑按位异或操作,操作的结果是如果某位不同则该位为1, 否则该位为0。
(摘自百度百科)
例如:求9^3(很多新手会误以为是 9 3 9^3 93)
1001
0011
-----
1010
记 ⊕ \oplus ⊕ 为异或运算,异或运算满足以下性质:
- x ⊕ x = 0 x \oplus x = 0 x⊕x=0
- x ⊕ y = y ⊕ x x \oplus y = y \oplus x x⊕y=y⊕x(交换律);
- ( x ⊕ y ) ⊕ z = x ⊕ ( y ⊕ z ) (x \oplus y) \oplus z = x \oplus (y \oplus z) (x⊕y)⊕z=x⊕(y⊕z)(结合律);
- x ⊕ y ⊕ y = x x \oplus y \oplus y = x x⊕y⊕y=x(自反性);
- ∀ i ∈ Z ∀ i ∈ Z , 有 4 i ⊕ ( 4 i + 1 ) ⊕ ( 4 i + 2 ) ⊕ ( 4 i + 3 ) = 0 \forall i \in Z∀i∈Z,有 4i \oplus (4i+1) \oplus (4i+2) \oplus (4i+3) = 0 ∀i∈Z∀i∈Z,有4i⊕(4i+1)⊕(4i+2)⊕(4i+3)=0
应用:
- 利用按位异或的自反性,我们可以不需要用辅助变量也能实现两个数的交换(装逼用【被打】)
#include <iostream> using namespace std; int main() { int a = 10, b = 5; a = a ^ b; b = a ^ b; a = a ^ b; cout << a << " " << b << endl; return 0; }
输出结果
5 10
4. 左移 <<
a<<n 表示将 a 的二进制表示数向左移动 n 位,即在后面添加 n 个 0。同样,也就相当于 a 乘以 2 n 2^n 2n。
#include <iostream>
using namespace std;
int main()
{
for (int i = 0; i < 10; i++)
cout << (1<<i) << " ";
return 0;
}
输入结果
1 2 4 8 16 32 64 128 256 512
5. 右移 >>
a>>n 与上面的左移刚好相反,表示将a的二进制表示数往右移动n位,即直接去掉a后面的n位,相当于a除以
2
n
2^n
2n。通常我们可以利用 n>>=1 来代替 n /= 2
来提高运算效率。