[知识点] 6.1 位运算与进位制
总目录 > 6 数学 > 6.1 位运算与进位制
前言
开始新的一部分。。。暑假开始要进行一些线上练习赛,数学向来也是涉猎不多也不擅长的部分,现在还是要花点时间好好补一下。
子目录列表
1、位运算
2、进位制
6.1 位运算与进位制
1、位运算
① 概念
在 1.2 C++ 基础知识 中,介绍了常用的各种运算符,其中一类被称为位运算符。位运算是基于整数,但以二进制来表示的运算。那么为什么要费尽周折把简单的十进制转换成二进制表示?因为计算机内部是以二进制存储数据,从计算机角度而言,二进制运算才是效率最高的,所以如果希望提升程序运行效率,肯定只能是使用十进制的人类去迁就二进制的计算机啦。
② 与、或、异或
不同于加减乘除 —— 十进制的四则基本运算,二进制有其不同的基本运算符,它们分别是与、或、异或。这个学期的《计算机组成与结构》、《数字电子技术》、《汇编语言》几门课反反复复地提到了这些概念,足以见得在二进制运算中举足轻重的地位。
手算时,需要将数转换成二进制,并逐位进行计算,最后将计算结果再转换成十进制。
> 与
符号:a & b
含义:对应位均为 1 时结果为 1
结果:1 & 1 = 1, 1 & 0 = 0 & 1 = 0 & 0 = 0
举例:5 & 6 = (101)2 & (110)2 = (100)2 = 4
> 或
符号:a | b
含义:对应位存在一个 1 时结果为 1
结果:1 | 1 = 1 | 0 = 0 | 1 = 1, 0 | 0 = 0
举例:5 | 6 = (101)2 | (110)2 = (111)2 = 7
> 异或
符号:a ^ b
含义:两个对应位不同时结果为 1
结果:1 ^ 0 = 0 ^ 1 = 1, 1 ^ 1 = 0 ^ 0 = 0
举例:5 ^ 6 = (101)2 ^ (110)2 = (011)2 = 3
③ 取反
符号:~num
含义:对num 的每一位取反
备注:实际使用并不多,同时牵涉到补码的概念,不多说
④ 左移和右移
> 左移
符号:num << i
含义:将 num 的二进制表示向左移动 i 位
举例:5 << 1 = (101)2 << 1 = (1010)2 = 10, 4 << 3 = (100)2 << 3 = (100000)2 = 32
从举例中不难看出,num 左移 i 位其实等价于 num 与 2 的 i 次方相乘。又已知位运算效率最高,所以相比 num * 2,num << 1 是更优的写法。
> 右移
符号:num >> i
含义:将 num 的二进制表示向左移动 i 位
举例:6 >> 1 = (110)2 >> 1 = (11)2 = 3, 9 >> 2 = (1001)2 >> 2 = (10)2 = 2
和左移相对,num 右移 i 位等价于 num 除以 2 的 i 次方并向下取整。
关于左移右移的诸多未定义情况,在 C++ 的不同版本里均不同,此处暂略。
⑤ 应用
上面多次提到,位运算效率高于其他运算,所以在以追求运算速度的需求下,可以将许多常规的写法替换为位运算写法,下面提供诸多常用位运算实现的功能:
> 取绝对值
int abs(int o) { return (o ^ (o >> 31)) - (o >> 31); // return o > 0 ? o : -o; 常规写法 }
> 判断符号是否相同
bool isSameSign(int x, int y) { // 有 0 的情况例外 return (x ^ y) >= 0; }
> 交换两个整数
void swap(int &a, int &b) { a ^= b ^= a ^= b; /* 常规写法 int tmp = a; a = b, b = tmp; */ }
> 获取二进制数的某一位
int getBit(int a, int b) { return (a >> b) & 1; }
> 求两个数的最大值 / 最小值
int max(int a, int b) { return b & ((a - b) >> 31) | a & (~(a - b) >> 31); // return a > b ? a : b; 常规写法 } int min(int a, int b) { return a & ((a - b) >> 31) | b & (~(a - b) >> 31); // return a < b ? a : b; 常规写法 }
位运算还可以用来进行状态压缩(请参见:<施工中>),或者题目本身需要位运算,比如快速幂(请参见:6.2 快速幂)。
2、进位制
在计算机中,二进制是内部运算所采用的进制,除此之外,八进制和十六进制也使用的较多。
这里进制之间的转换就不再多说了,提一下在 C++ 中如何表示这两种进制。
对于八进制,在数之前加上 "ox",例如八进制数 (123)8,在 C++ 中的书写形式为 "ox123";
对于十六进制,加上 "0x",例如十六进制数 (ABC)16,在 C++ 中的书写形式为 "0xABC"(大小写均可)。
那么平时在什么时候可能需要用到这些进制呢?
众所周知 int 的数据范围为 2 ^ 63 - 1,用十六进制表示为 0x7fffffff。许多时候我们需要使用到一个“无穷大”的概念,但显然计算机并不接受这种说法,毕竟它并非一个具体数值。这时候,我们可以将数据类型范围最大值近似等价为无穷大,即 0x7fffffff。但这并非最好的数值。尽管数据不会超过这个范围,但有时难免出现使用这个“无穷大”与另一个数相加的情况,而一旦出现,则直接溢出导致错误,所以我们退而求其次,选择一个相对较小的数来充当这个角色:
0x3f3f3f3f
它在算法竞赛中使用广泛,因为确实太好用了。它的优势在于:
① 十进制表示为 1061109567,属于 10 ^ 9 数量级,且刚好大于许多题目规定的数据范围 10 ^ 9;
② 它的 2 倍数为 2122219134,刚好小于 int 范围最大值,所以倘若出现“无穷大”相加的情况,同样不会溢出;
③ 使用 memset 初始化数组时,可以直接 memset(a, 0x3f, sizeof(a)),因为它的每个字节都是重复的 —— 0x3f。
一般情况下,double 类型的数据也可以使用上述十六进制数。
1 << 30 同样适用于表示 int 范围的无穷大,但不支持浮点类型