Bit Twiddling Hacks 常用位运算技巧总结#
来源整理自 Bit Twiddling Hacks,这里只归纳工程里最常见、最实用的一批技巧。
风格保持和 common-algorithm-patterns.md 一致:每个技巧只保留 解释 + 代码框架(C++)。
0) 使用前提(很重要)#
位运算技巧很依赖整数表示和移位语义。为了减少未定义行为和平台差异:
- 优先使用
uint32_t/uint64_t 等无符号类型做位运算。 - 右移有符号负数在标准里是实现定义(不同编译器/架构可能不同)。
- 涉及溢出的表达式(如
x - y)要先考虑范围。
代码框架#
1
2
3
4
5
6
7
| #include <bit>
#include <cstdint>
#include <limits>
using u32 = uint32_t;
using u64 = uint64_t;
using i32 = int32_t;
|
1) 判断符号 / 是否异号#
- 符号:常见返回
-1/0/1 三值。 - 异号:两个数符号位不同,则
x ^ y 的最高位为 1(在补码机器上很常用)。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
| #include <cstdint>
int sign3(int x) {
// 返回 -1 / 0 / 1,分支少且可读性高
return (x > 0) - (x < 0);
}
bool oppositeSigns(int x, int y) {
// 常见位技巧(依赖补码/符号位语义)
return (x ^ y) < 0;
}
|
2) 无分支 abs / min / max#
这类写法常见于追求分支预测稳定性的场景。现代编译器对普通 std::abs/std::min/std::max 也很强,实战要基准测试后再决定是否上 hack。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <cstdint>
int absNoBranch(int v) {
int mask = v >> 31; // 若 v<0,mask 全1;否则全0(实现相关)
return (v ^ mask) - mask; // 或 (v + mask) ^ mask
}
int minNoBranch(int x, int y) {
return y ^ ((x ^ y) & -(x < y));
}
int maxNoBranch(int x, int y) {
return x ^ ((x ^ y) & -(x < y));
}
|
3) 判断 2 的幂 / 提取 lowbit#
x & (x - 1) 会清掉最低位的 1。- 因此
x != 0 && (x & (x - 1)) == 0 表示只有一位为 1,即 2 的幂。 x & -x 可提取最低位 1(Fenwick 树等常用)。
代码框架#
1
2
3
4
5
6
7
8
9
| #include <cstdint>
bool isPowerOfTwo(uint32_t x) {
return x && ((x & (x - 1)) == 0);
}
uint32_t lowbit(uint32_t x) {
return x & (~x + 1); // 等价于 x & -x
}
|
3.5) 异或核心恒等式(a ^ a = 0)与两道高频题模板#
ref/fucking-algorithm/算法思维系列/常用的位操作.md 里非常强调这一组性质,它确实是算法题高频:
a ^ a = 0a ^ 0 = a- 异或满足交换律/结合律
这三条足够解决大量“成对出现 + 一个落单”类题目。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <vector>
using namespace std;
int singleNumber(const vector<int>& nums) {
int x = 0;
for (int v : nums) x ^= v;
return x;
}
int missingNumber(const vector<int>& nums) {
// nums 长度为 n,元素应覆盖 [0..n] 且缺一个
int n = (int)nums.size();
int x = n;
for (int i = 0; i < n; ++i) {
x ^= i ^ nums[i];
}
return x;
}
|
3.6) index & (len - 1) 环形数组技巧#
你引用的那篇里这个技巧也很实用:当 len 是 2 的幂时,index & (len - 1) 等价于 index % len。
适合 ring buffer / 循环数组热路径。若 len 不是 2 的幂,这个技巧不成立。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <cassert>
#include <cstdint>
#include <vector>
using namespace std;
bool isPowerOfTwo(size_t x) {
return x && ((x & (x - 1)) == 0);
}
int circularAt(const vector<int>& arr, int64_t index) {
size_t n = arr.size();
assert(isPowerOfTwo(n)); // 前提条件
size_t pos = static_cast<size_t>(index) & (n - 1);
return arr[pos];
}
|
4) 条件置位 / 清位 / 按掩码合并#
这类技巧用于“按条件更新某些位”,避免 if 分支。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
| #include <cstdint>
uint32_t setOrClearByFlag(uint32_t w, uint32_t mask, bool flag) {
// flag=true -> 置位;false -> 清位
// 常见分支消除写法
return (w & ~mask) | (static_cast<uint32_t>(-static_cast<int>(flag)) & mask);
}
uint32_t mergeByMask(uint32_t a, uint32_t b, uint32_t mask) {
// mask 为 1 的位来自 b,否则来自 a
return (a & ~mask) | (b & mask);
}
|
4.5) 位掩码状态操作(增删查改)#
在子集枚举、DP 状压、权限位里是必备模板:
- 查第 k 位
- 置 1 / 清 0 / 翻转
- 清除最低位 1
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <cstdint>
bool testBit(uint32_t x, int k) {
return (x >> k) & 1u;
}
uint32_t setBit(uint32_t x, int k) {
return x | (1u << k);
}
uint32_t clearBit(uint32_t x, int k) {
return x & ~(1u << k);
}
uint32_t toggleBit(uint32_t x, int k) {
return x ^ (1u << k);
}
uint32_t clearLowestOne(uint32_t x) {
return x & (x - 1);
}
|
5) 统计 1 的个数(popcount)#
最常用两种:
- Kernighan:每轮消一个 1,复杂度和 1 的个数相关。
std::popcount:C++20 标准库,通常会映射到 CPU 指令。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <bit>
#include <cstdint>
int popcountKernighan(uint32_t x) {
int cnt = 0;
while (x) {
x &= (x - 1);
++cnt;
}
return cnt;
}
int popcountStd(uint32_t x) {
return std::popcount(x);
}
|
6) 奇偶校验(parity)#
parity = popcount(x) % 2。
可用折叠异或快速压缩到 1 bit。
代码框架#
1
2
3
4
5
6
7
8
9
10
| #include <cstdint>
int parity32(uint32_t x) {
x ^= x >> 16;
x ^= x >> 8;
x ^= x >> 4;
x ^= x >> 2;
x ^= x >> 1;
return x & 1; // 1=奇数个1,0=偶数个1
}
|
7) 末尾 0 个数(ctz)/ 最高位位置(log2)#
ctz 常用于位图、稀疏状态、lowbit 跳转。floor(log2(x)) 本质是最高位 1 的位置。- 推荐
std::countr_zero / std::bit_width(C++20)。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
| #include <bit>
#include <cstdint>
int ctz32(uint32_t x) {
if (x == 0) return 32; // 约定返回位宽
return std::countr_zero(x);
}
int floorLog2(uint32_t x) {
// x > 0
return std::bit_width(x) - 1;
}
|
8) 向上取整到最近 2 的幂#
经典“位扩散”技巧:先把最高位 1 右边全部填满,再 +1。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
| #include <cstdint>
uint32_t roundUpPow2(uint32_t x) {
if (x <= 1) return 1;
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x + 1;
}
|
9) 位反转(bit reverse)#
常见于 FFT/图像/编码场景。
简单写法是逐位搬运;追求极致性能可用查表或并行交换技巧。
代码框架#
1
2
3
4
5
6
7
8
9
10
| #include <cstdint>
uint32_t reverseBits32(uint32_t x) {
uint32_t r = 0;
for (int i = 0; i < 32; ++i) {
r = (r << 1) | (x & 1u);
x >>= 1;
}
return r;
}
|
10) 字(word)内字节检测(zero-byte trick)#
在一个 32/64 位整数里并行判断“某字节是否为 0”,常用于字符串扫描优化。
经典式:((x - 0x0101...) & ~x & 0x8080...) != 0
代码框架#
1
2
3
4
5
| #include <cstdint>
bool hasZeroByte(uint32_t x) {
return ((x - 0x01010101u) & ~x & 0x80808080u) != 0;
}
|
11) 模 2^s 与模 (2^s - 1)#
x % (1 << s) 可直接用掩码:x & ((1<<s)-1)。% (2^s - 1) 有折叠求和技巧(页面有多版本),常见于特定编码/校验优化。
代码框架#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <cstdint>
uint32_t modPow2(uint32_t x, unsigned s) {
return x & ((1u << s) - 1u);
}
uint32_t modPow2Minus1(uint32_t x, unsigned s) {
// 通用折叠框架(非最优版,重在思路)
uint32_t d = (1u << s) - 1u;
while (x > d) {
x = (x & d) + (x >> s);
}
return (x == d) ? 0 : x;
}
|
12) 下一个同 1 个数的排列(snoob)#
给定一个 bitmask,求字典序下一个且“1 的个数相同”的 bitmask。
组合枚举、子集状态迭代里非常有用。
代码框架#
1
2
3
4
5
6
7
8
| #include <cstdint>
uint32_t nextBitPermutation(uint32_t v) {
uint32_t c = v & -v;
uint32_t r = v + c;
if (r == 0) return 0; // 溢出,无下一个
return (((r ^ v) >> 2) / c) | r;
}
|
13) 实战建议:优先标准库,再用 hacks#
bithacks 的核心价值是“理解位级等价变换”。但工程上推荐:
- 先写可读版本;
- 优先
std::popcount/std::countr_zero/std::bit_width; - 只在性能瓶颈处引入 hack,并附注释和基准测试结果。
代码框架#
1
2
3
4
| // 1) baseline 可读实现
// 2) profiler 发现热点
// 3) 替换成位技巧或 intrinsic
// 4) 基准测试 + 单元测试(边界值:0、INT_MIN、全1、单bit)
|