位运算的奇技淫巧(二)
位运算就是基于整数的二进制表示进行的运算。由于计算机内部就是以二进制来存储数据,位运算是相当快的。
之前有总结过位运算的技巧,但稍微对以前写的文章不太满意,所以重新总结一下
常用的运算符共 6 种,分别为与( &
)、或( |
)、异或( ^
)、取反( ~
)、左移( <<
)和右移( >>
)。
与、或、异或
与( &
)或( |
)和异或( ^
)这三者都是两数间的运算,因此在这里一起讲解。
它们都是将两个整数作为二进制数,对二进制表示中的每一位逐一运算。
运算符 | 解释 |
---|---|
& |
只有两个对应位都为 1 时才为 1 |
` | ` |
^ |
只有两个对应位不同时才为 1 |
异或运算的逆运算是它本身,也就是说两次异或同一个数最后结果不变,即
举例:
取反
取反是对一个数
~
把 ~
运算中同样会取反。
补码:在二进制表示下,正数和 0 的补码为其本身,负数的补码是将其对应正数按位取反后加一。
举例(有符号整数):
左移和右移
num << i
表示将
num >> i
表示将
举例:
在 C++ 中,右移操作中右侧多余的位将会被舍弃,而左侧较为复杂:对于无符号数,会在左侧补 0;而对于有符号数,则会用最高位的数(其实就是符号位,非负数为 0,负数为 1)补齐。左移操作总是在右侧补 0。
复合赋值位运算符
和 +=
, -=
等运算符类似,位运算也有复合赋值运算符: &=
, |=
, ^=
, <<=
, >>=
。(取反是单目运算,所以没有。)
关于优先级
位运算的优先级低于算术运算符(除了取反),而按位与、按位或及异或低于比较运算符,所以使用时需多加注意,在必要时添加括号。
位运算的应用
位运算一般有三种作用:
-
高效地进行某些运算,代替其它低效的方式。
-
表示集合。(常用于 状压 DP 。)
-
题目本来就要求进行位运算。
需要注意的是,用位运算代替其它运算方式(即第一种应用)在很多时候并不能带来太大的优化,反而会使代码变得复杂,使用时需要斟酌。(但像“乘 2 的非负整数次幂”和“除以 2 的非负整数次幂”就最好使用位运算,因为此时使用位运算可以优化复杂度。)
乘 2 的非负整数次幂
int mulPowerOfTwo(int n, int m) { // 计算 n*(2^m)
return n << m;
}
除以 2 的非负整数次幂
int divPowerOfTwo(int n, int m) { // 计算 n/(2^m)
return n >> m;
}
!!! warning
我们平常写的除法是向 0 取整,而这里的右移是向下取整(注意这里的区别),即当数大于等于 0 时两种方法等价,当数小于 0 时会有区别,如: -1 / 2
的值为 -1 >> 1
的值为
判断一个数是不是 2 的正整数次幂
bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
对 2 的非负整数次幂取模
int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }
取绝对值
在某些机器上,效率比 n > 0 ? n : -n
高。
int Abs(int n) {
return (n ^ (n >> 31)) - (n >> 31);
/* n>>31 取得 n 的符号,若 n 为正数,n>>31 等于 0,若 n 为负数,n>>31 等于 -1
若 n 为正数 n^0=n, 数不变,若 n 为负数有 n^(-1)
需要计算 n 和 -1 的补码,然后进行异或运算,
结果 n 变号并且为 n 的绝对值减 1,再减去 -1 就是绝对值 */
}
取两个数的最大/最小值
在某些机器上,效率比 a > b ? a : b
高。
// 如果 a>=b,(a-b)>>31 为 0,否则为 -1
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); }
判断符号是否相同
bool isSameSign(int x, int y) { // 有 0 的情况例外
return (x ^ y) >= 0;
}
交换两个数
void swap(int &a, int &b) { a ^= b ^= a ^= b; }
获取一个数二进制的某一位
// 获取 a 的第 b 位,最低位编号为 0
int getBit(int a, int b) { return (a >> b) & 1; }
表示集合
一个数的二进制表示可以看作是一个集合(0 表示不在集合中,1 表示在集合中)。比如集合 {1, 3, 4, 8}
,可以表示成
操作 | 集合表示 | 位运算语句 |
---|---|---|
交集 | a & b |
|
并集 | `a | |
补集 | ~a (全集为二进制都是 1) |
|
差集 | a & (~b) |
|
对称差 | a ^ b |
二进制的状态压缩
二进制状态压缩,是指将一个长度为
操作 | 运算 |
---|---|
取出整数 n 在二进制表示下的第 k 位 | (n >> k) & 1 |
取出整数n 在二进制表示下的第 0 ~ k - 1 位 (后 k 位) | n & ((1 << k) - 1) |
对整数 n 在二进制表示下的第 k 位取反 | n xor (1 << k) |
对整数 n 在二进制表示下的第 k 位赋值 1 | n | (1 << k) |
对整数 n 在二进制表示下的第 k 位赋值 0 | n & (~(1 << k)) |
这种方法运算简便,并且节省了程序运行的时间和空间。当m不太大时,可以直接使用一个整数类型存储。当m较大时,可以使用若干个整数类型(int数组),也可以直接利用
遍历某个集合的子集
// 遍历 u 的非空子集
for (int s = u; s; s = (s - 1) & u) {
// s 是 u 的一个非空子集
}
用这种方法可以在
内建函数
GCC 中还有一些用于位运算的内建函数:详细文章介绍
-
int __builtin_ffs(int x)
:返回 的二进制末尾最后一个 的位置,位置的编号从 开始(最低位编号为 )。当 为 时返回 。 -
int __builtin_clz(unsigned int x)
:返回 的二进制的前导 的个数。当 为 时,结果未定义。 -
int __builtin_ctz(unsigned int x)
:返回 的二进制末尾连续 的个数。当 为 时,结果未定义。 -
int __builtin_clrsb(int x)
:当 的符号位为 时返回 的二进制的前导 的个数减一,否则返回 的二进制的前导 的个数减一。 -
int __builtin_popcount(unsigned int x)
:返回 的二进制中 的个数。 -
int __builtin_parity(unsigned int x)
:判断 的二进制中 的个数的奇偶性。
这些函数都可以在函数名末尾添加 l
或 ll
(如 __builtin_popcountll
)来使参数类型变为 ( unsigned
) long
或 ( unsigned
) long long
(返回值仍然是 int
类型)。
例如,我们有时候希望求出一个数以二为底的对数,如果不考虑 0
的特殊情况,就相当于这个数二进制的位数 -1
,而一个数 n
的二进制表示的位数可以使用 32-__builtin_clz(n)
表示,因此 31-__builtin_clz(n)
就可以求出 n
以二为底的对数。
由于这些函数是内建函数,经过了编译器的高度优化,运行速度十分快(有些甚至只需要一条指令)。
更多位数
如果需要操作的集合非常大,可以使用 bitset容器
。
题目推荐
参考
位运算技巧: https://graphics.stanford.edu/~seander/bithacks.html
Other Builtins of GCC: https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~