位运算的使用场景
1 性能优化的乘除与奇偶判断
对于 2 的幂次的乘除,位移运算比普通乘除法快数十倍(虽然现代编译器会优化,但位运算语义更明确)。
x = 100
# 乘以 2 的 n 次方 → 左移
fast_mul_8 = x << 3 # 相当于 100 * 8 = 800
# 除以 2 的 n 次方(向下取整)→ 右移
fast_div_4 = x >> 2 # 相当于 100 // 4 = 25
偶数二进制最低位一定为 0,奇数数二进制最低位一定为 1。在判断奇偶数时可以用 & 运算来判断,执行效率高。
- 可读性 vs 性能:在非瓶颈代码中,
x % 2 == 0比x & 1更直观。仅在循环极深或底层库中使用位运算优化。
# 奇偶判断(比 x % 2 == 0 更快)
is_even = (x & 1) == 0
is_odd = (x & 1) == 1
[!note] 原理
一个二进制数:01010
在二进制表示中,最低位(第 0 位)对应的是 $2^0=1$。
- 如果一个整数的最低位是 0,那么它就可以写成 $2×k$ 的形式,因此是偶数;
- 如果最低位是 1,那么它就可以写成 $2×k+1$ 的形式,因此是奇数。
所以,通过检查最低位就能直接判断奇偶性。
2 权限管理与多状态标志位
用一个整数的不同二进制位表示 开启/关闭 状态,可大幅节省内存并提高检查速度(常见于 Linux 文件权限、Django 模型权限)。
# 定义权限常量(2 的幂,互不干扰)
READ = 0b100 # 4
WRITE = 0b010 # 2
EXEC = 0b001 # 1
# 用户权限:读 + 写
user_perm = READ | WRITE # 6 (0b110)
# 检查是否有写权限
if user_perm & WRITE:
print("拥有写权限")
# 添加执行权限
user_perm |= EXEC # 变为 7 (0b111)
# 移除写权限
user_perm &= ~WRITE # 变为 5 (0b101)
# 切换(反转)执行权限
user_perm ^= EXEC
3 哈希算法、加密与数据混淆
异或(XOR)是很多加密算法(如 AES 部分步骤、流密码)和哈希(如 FNV-1a)的核心操作,利用其 自反性(a ^ b ^ b == a)实现可逆变换。
4 位图(Bitmap)与布隆过滤器
用一个 bit 表示一个元素是否存在,实现极致的空间压缩(10 亿个标志位仅需约 119 MB)。
# 简易位图:记录用户 ID 是否签到
class BitMap:
def __init__(self, max_val):
self.bits = bytearray((max_val >> 3) + 1)
def set(self, num):
self.bits[num >> 3] |= (1 << (num & 7))
def test(self, num):
return bool(self.bits[num >> 3] & (1 << (num & 7)))
bm = BitMap(1000000)
bm.set(88888)
print(bm.test(88888)) # True
print(bm.test(99999)) # False
5 算法竞赛与 LeetCode 中的数学技巧
许多算法题的 最优解 依赖于位运算性质,例如找单身数字、计算汉明重量等。
# ==========找 “只出现一次的数字”==========
l = [1, 2, 1, 3, 2]
# 方法一
res = 0
for num in l:
res ^= num
print(res)
# 方法二
res1 = reduce(lambda x, y: x ^ y, l)
print(res1)
5.1 计算二进制中 1 的个数(汉明重量)
核心技巧:x &= x - 1
这个方法的核心在于:每次执行该操作,都会把当前二进制数最右边(最低位)的一个 1 变成 0。
数学原理与证明
假设任意一个二进制数 x,其最低位的 1 位于第 k 位(从右往左数,最右为第 0 位)。那么 x 可以表示为:
x = ... 1 [k个0]
即:从第 0 位到第 k-1 位全是 0,第 k 位是 1,更高位任意。
当我们计算 x - 1 时:
- 最低位的
1(第k位)因为不够减,需要向高位借位,所以它变成了0。 - 原本右侧的
k个0因为借位的关系,全部变成了1。 - 更高位保持不变。
对比图例:
| 二进制状态 | 高位部分 | 第 k 位 | 低位部分 (k 个位) |
|---|---|---|---|
| x | …保持不变… | 1 | 0 0 … 0 |
| x-1 | …保持不变… | 0 | 1 1 … 1 |
接下来执行 x & (x - 1) (按位与运算):
- 高位部分:与自身相与,保持不变。
- 第 k 位:
1 & 0 = 0。 - 低位部分:
0 & 1 = 0。
结论:x & (x - 1) 的结果等于将 x 的最低有效 1 置为 0,其余位完全不变。
执行过程演示
以数字 x = 42 (二进制 101010)为例:
| 循环轮次 | 当前值 (x) 二进制 | 操作 x & (x-1) 的计算过程 |
结果 (新 x) | 计数器变化 |
|---|---|---|---|---|
| 初始 | 101010 (42) |
0 | ||
| 第 1 次 | 101010 |
101010& 101001---------101000 |
101000 (40) |
1 |
| 第 2 次 | 101000 |
101000 & 100111 --------- 100000 |
100000 (32) |
2 |
| 第 3 次 | 100000 |
100000 & 011111 --------- 000000 |
000000 (0) |
3 |
循环结束,共执行 3 次,恰好等于 42 二进制中 1 的个数。
代码实现与复杂度
def hamming_weight(n: int) -> int:
count = 0
while n:
n &= n - 1
count += 1
return count
- 时间复杂度:O(k),其中 k 是二进制中
1的个数。对于稀疏的二进制数(如0b100000000),只循环 1 次,远优于逐位右移 O(log n) 的方法。 - 现代 Python 最佳实践:Python 3.8+ 可直接调用内置方法
n.bit_count(),由 C 语言底层实现,速度极快。
5.2 判断是否为 2 的整数次幂
核心技巧:n > 0 and (n & (n - 1)) == 0
数学原理
一个数是 2 的整数次幂(即 $2^k$),其二进制表示具有独一无二的结构:
- 结构:
1后面紧跟k个0。 - 举例:
1$(2^0)$ =1,2$(2^1)$ =10,4$(2^2)$ =100,8$(2^3)$ =1000。
基于上一节推导的 x - 1 的性质:
- 对于 $2^k$(二进制
100..),减去 1 后,那个唯一的1变成0,右侧所有的0变成1。 - 例如
8(1000) -1=7(0111)。
此时将两者进行 & 运算:
1000 (8)
& 0111 (7)
-----------
0000 (0)
结论:对于任何 2 的幂次方,n & (n - 1) 的结果一定为 0。
边界条件与反例
-
为什么需要
n > 0?
如果n = 0,0 & (-1)在 Python 中结果是0,会误判为 2 的幂次,但 0 不是 2 的幂。 -
为什么非 2 的幂次结果一定不为 0?
如果n不是 2 的幂次,其二进制至少包含 两个不同的 1。执行n & (n-1)只能消除 最低位的 1,至少还会剩下一个其他的 1,结果必然大于 0。
执行过程对比演示
| 数值 (n) | 二进制表示 | n - 1 二进制表示 | n & (n - 1) 结果 | 是否为 2 的幂? |
|---|---|---|---|---|
| 8 | 1000 |
0111 |
0000 (0) |
✅ 是 |
| 12 | 1100 |
1011 |
1000 (8) |
❌ 否 |
| 0 | 0000 |
-1 (全 1) |
0000 (0) |
❌ 否 (需显式排除) |
| -8 | 补码复杂 | 补码复杂 | 非零 | ❌ 否 |
代码实现
def is_power_of_two(n: int) -> bool:
# 必须确保 n 为正整数,并且符合位运算特性
return n > 0 and (n & (n - 1)) == 0
# 测试
print(is_power_of_two(16)) # True
print(is_power_of_two(18)) # False
print(is_power_of_two(0)) # False
print(is_power_of_two(1)) # True (2^0)

浙公网安备 33010602011771号