一篇文章了解 机器数 & 位运算
聪明的你在努力成为一位 OIerOIerOIer 的过程中,一定会遇到这些奇奇怪怪的名词。虽然看起来不知道是什么意思,但理解了就很容易了。
机器数(Computer Number)
机器数一般分为原码,反码 ,补码三种,是为机器存储数据而设计的特殊二进制代码。
这篇文章说的都是有符号的整数,最左端的一位是符号位。以8位整数举例。
这里先简单说一下二进制吧。
二进制(Binary)
为了方便计算机存储数据,科学家发明了二进制(Binary),它只需要0
和1
就能表示任何整数。二进制中的一个0
或1
就被称为“位”(Binary digit,简称"Bit")
具体的转换方法:
- 十进制转二进制:每次取模
2
获得余数,存在数组里再把要转换的数除以2
。最后反过来就是它的二进制了。
// #include <vector>
// #include <algorithm>
std::vector<bool> dec_to_bin(int k)
{
if (k == 0)
{
return std::vector<bool>(1, 0);
}
std::vector<bool> res;
while (k)
{
res.push_back(k % 2);
k /= 2;
}
std::reverse(res.begin(), res.end());
return res;
}
- 二进制转十进制:按权展开法,倒序遍历,权从 1 开始,如果这位是 1 ,就加上权,每次循环结束后都把权乘 2 。
// #include <vector>
int bin_to_dec(std::vector<bool> b)
{
int p = 1;
int res = 0;
for (auto it = b.rbegin(); it != b.rend(); it++)
{
if (*it == 1)
{
res += p;
}
p *= 2;
}
return res;
}
原码(Sign-Magnitude)
正数的原码就是其二进制,若不足8位则补0
负数的原码需要在其二进制上将最左端的符号位设为1,表示这是负数。
例:
10 转为二进制:
0 0 0 0 1 0 1 0
所以 10 的 8 位原码是:
0 0 0 0 1 0 1 0
-10 是负数,需要将原数的符号位设为 1。
所以 -10 的 8 位原码是:
1 0 0 0 1 0 1 0
^
最左边的这个就是符号位
原码的优点
容易理解,最接近普通二进制,转换方便。
原码的缺点
对于电脑来说,原码的负数就不太好理解了。
反码(Ones’ Complement)
正数的反码与原码相同。
负数的反码是对原码除符号位外按位取反。
例:
10 的原码是:
0 0 0 0 1 0 1 0
正数反码与原码相同,所以 10 的反码是:
0 0 0 0 1 0 1 0
-10 的原码是
1 0 0 0 1 0 1 0
负数补码需要按位取反,所以 -10 的反码是:
1 1 1 1 0 1 0 1
反码的优点
对于机器来说,反码让减法和负数的实现更简单了。
反码的缺点
对于人来说,负数的反码转换时会难一些。
补码(Two’s Complement)
正数的补码与原码相同。
负数的补码是他的原码按位取反(除符号位)后 + 1 (反之亦然)。
例:
10 的原码是:
0 0 0 0 1 0 1 0
正数补码与原码相同,所以 10 的补码是:
0 0 0 0 1 0 1 0
-10 的原码是:
1 0 0 0 1 0 1 0
负数补码是其原码按位取反后 + 1,所以是
1 1 1 1 0 1 0 1 + 1
=1 1 1 1 0 1 1 0
补码的优点
计算机使用的就是补码,因为它对于计算机的运算是最便捷的,补码可以将减法转换为加法。
例:a + b = a + (~b + 1)
,其中~
表示按位取反,这样不用判断a
和b
大小关系,优化了算法。自己多试一试就能发现它的好处。
补码的缺点
对于人来说,负数的补码转换时会难一些,因为这需要进行两步操作。
位运算(Bitwise Operation)
位运算和机器数有着很大的关联。二进制数据中的一个0
或1
就被称为“位”。
相比于四则运算,位运算会快很多,因为它的操作更接近计算机底层。
需要注意的是,位运算的优先级都很低,所以尽量加括号,不然可能报错。
左移、右移(<<, >>)
顾名思义,他们会对原数的补码左移或右移,相当于*2
和/2
,但速度更快。
左移过多可能导致溢出,右移不会改动符号位。
格式:
int a = 1;
a <<= 5; // a左移5位,现在应该是32
a >>= 3; // a右移3位,现在应该是4
按位与(AND)、或(OR、非(NOT)(&, |, !)
和逻辑上的差不多,但会对每一位进行操作。
- 与:当两个都是 1 时则输出 1。
- 或:当其中存在 1 时则输出 1.
- 非:1 变 0 , 0 变 1。
按位与和按位或是双目运算符,按位非(按位取反)是单目运算符。
注:这三个操作会影响符号位。比如:一个整数按位取反以后会变成负数。
格式:
int _or = (10 | 7);
// 10 = 1 0 1 0 (2)
// 7 = 0 1 1 1 (2)
// 10 | 5 = 1 1 1 1 (2) = 15
// 所以 _or 现在是 15
int _and = (10 | 7);
// 10 = 1 0 1 0 (2)
// 7 = 0 1 1 1 (2)
// 10 & 5 = 0 0 1 0 (2) = 2
// 所以 _and 现在是 2
short _not = (~10);
// 0000 0000 0000 1010 (2)
// =1111 1111 1111 0101 (2)
// =-11 (10)
// 所以现在 _not 是 -11,前文说过怎么把负数(补码)转为正数。
按位异或(^,XOR)
当两位不同时则输出 1 ,相同则输出 0 。
这是一个比较特殊的运算符,因为它有很多神奇的特点。
异或的几个性质:
(a ^ b) ^ c = a ^ (b ^ c)
符合结合律,可以按任意顺序异或x ^ x=0,x ^ 0 = x
一个数异或自己等于0
,异或0
等于本身a ^ b ^ b = a ^ 0 = a
有偶数个相同的数取异或相当于异或0
(推论)
格式:
int _xor = (10 ^ 7);
// 10 = 1 0 1 0 (2)
// 7 = 0 1 1 1 (2)
// 10 ^ 7 = 1 1 0 1 (2) = 13
// 所以 _xor 现在是 13
因为异或的各种特性,有很多有用函数利用了异或,优化了执行效率。
如:
void _swap(int &x, int &y) // 交换两数
{
x = x ^ y;
y = x ^ y;
x = x ^ y;
}
int lowbit(int x) // 获取一个数在二进制中的最低位
{
return x & -x;
}
按位同或(XNOR)
和异或相反,相同则输出 1 ,不同则输出 0 。
虽然 C++ 中没有同或运算符,但是由于它和异或正好相反,所以可以使用!(a ^ b)
来实现同或。
以上就是 C++ 中的位运算。运用位运算,可以优化前文提到的二进制转换代码:
// #include <vector>
// #include <algorithm>
std::vector<bool> dec_to_bin(int k)
{
if (k == 0)
{
return std::vector<bool>(1, 0);
}
std::vector<bool> res;
while (k)
{
res.push_back(k & 1);
k >>= 1;
}
std::reverse(res.begin(), res.end());
return res;
}
int bin_to_dec(std::vector<bool> b)
{
int p = 1;
int res = 0;
for (auto it = b.rbegin(); it != b.rend(); it++)
{
if (*it == 1)
{
res += p;
}
p <<= 1;
}
return res;
}
以及一些函数:
int getbit(int x, int pos) // 获取某一位
{
return ((x >> pos) & 1);
}
int setbit(int x, int pos, int to)
{
if (getbit(x, pos) == to)
{
return x;
}
return (x ^ (1 << pos));
}
int _abs(int x) // 绝对值
{
return (x ^ (x >> 31)) - (x >> 31); // long long 就把 31 改成 63
}