cv.cm

编码 · 算法

位运算详解:运算符、技巧与示例

阅读约 7 分钟

简短回答

位运算是指用按位运算符直接操作整数的二进制位(bit):与(&)、或(|)、异或(^)、非(~)以及移位 << 和 >>。借助它们,你可以测试、置位、清零和翻转单个二进制位,把一组小整数存进一个整数里(位掩码),还能解决统计 1 的个数、在常数额外空间内找出唯一落单的数字等问题。对固定宽度的整数来说,每个操作都只是一条快速的 CPU 指令。

打开二进制转换

位运算解决什么问题

整数是一排二进制位,许多问题其实就是在处理这些位:rwx 这样的文件权限、功能开关、网络掩码、像素颜色、紧凑的选项集合、校验和与哈希。按位运算符让你一次操作就能读取和修改许多位,既节省内存又非常快。

面试题也常用到位运算技巧,因为它们能给出优雅的 O(1) 空间解法,例如在其他数字都出现两次时,找出只出现一次的那个数。

运算符

示例使用 a = 12(二进制 1100)和 b = 10(二进制 1010)。

运算符名称每一位的规则示例结果
a & b与两位都为 1 时结果为 11100 & 10101000 = 8
a | b或任一位为 1 时结果为 11100 | 10101110 = 14
a ^ b异或两位不同时结果为 11100 ^ 10100110 = 6
~a非翻转每一位~12-13(补码)
a << n左移位向左移动,右侧补 012 << 124(乘以 2ⁿ)
a >> n右移位向右移动12 >> 23(除以 2ⁿ 并向下取整)
a, b = 12, 10
print(bin(a), bin(b))         # 0b1100 0b1010
print(a & b, a | b, a ^ b)    # 8 14 6
print(~a, a << 1, a >> 2)     # -13 24 3
print(f"{a ^ b:04b}")         # 0110
print(int("1010", 2))         # 10

单个二进制位的操作

位的下标从最右边(最低有效位)开始,从 0 计数。掩码 1 << i 只有第 i 位为 1。

任务表达式
测试第 i 位(x >> i) & 1 或 x & (1 << i) != 0
置位第 i 位x | (1 << i)
清除第 i 位x & ~(1 << i)
翻转第 i 位x ^ (1 << i)
只保留最低位的 1x & -x
清除最低位的 1x & (x - 1)
x 是否为 2 的幂?(x > 0)x & (x - 1) == 0

实例:统计二进制中 1 的个数

Brian Kernighan 的方法不断清除最低位的 1,直到什么都不剩,所以循环次数等于 1 的个数,而不是位的个数。以 x = 44(二进制 101100)为例追踪:

步骤x(二进制)x - 1(二进制)x & (x - 1)计数
11011001010111010001
21010001001111000002
31000000111110000003
def count_bits(x):
    count = 0
    while x:
        x &= x - 1        # drop the lowest set bit
        count += 1
    return count

print(count_bits(44))     # 3
print((44).bit_count())   # 3
print((44).bit_length())  # 6

int.bit_count() 自 Python 3.10 起可用。int.bit_length() 返回用二进制表示该数所需的位数(不含符号)。

Python 中的经典技巧

from functools import reduce
from operator import xor

# Every number appears twice except one: pairs cancel out with XOR.
print(reduce(xor, [4, 1, 2, 1, 2]))   # 4

# Swap two integers without a temporary variable (for illustration only).
x, y = 5, 9
x ^= y; y ^= x; x ^= y
print(x, y)                            # 9 5

# Bitmask as a set: enumerate all subsets of 3 items.
items = ["a", "b", "c"]
for mask in range(1 << len(items)):
    print(mask, [items[i] for i in range(len(items)) if mask >> i & 1])
# 0 []
# 1 ['a']
# 2 ['b']
# 3 ['a', 'b']
# 4 ['c']
# 5 ['a', 'c']
# 6 ['b', 'c']
# 7 ['a', 'b', 'c']

掩码 0 是空集,5(二进制 101)对应 ['a', 'c'],7 则是全部三个元素。位掩码动态规划采用同样的思想,把掩码作为状态,适用于元素不超过约 20 个的问题,例如小规模输入上的旅行商问题。

另外两个经典用法:x & 1 对奇数为 1;把 0 到 n 的所有数与列表中的所有值一起做异或,可以找出那个缺失的数。

时间与空间复杂度

操作时间说明
对机器字长整数做 &、|、^、~ 和移位O(1)在 C、Java、Go 或 Rust 中是一条 CPU 指令。
对 Python int 做同样的操作O(位数)Python 整数没有大小上限;小数值依然非常快。
Kernighan 位计数O(1 的个数)在 Python 3.10+ 中,int.bit_count() 更快。
用掩码枚举所有子集O(n × 2ⁿ)2ⁿ 个掩码,每个要测试 n 位。

位掩码为每个可能的元素使用一个二进制位,所以 64 个标志可以放进一个 64 位整数,而不必用 64 个独立的布尔变量或列表元素。

常见错误

  • 负数:Python 的整数表现得好像有无穷多个符号位,所以 ~5 是 -6,bin(-5) 是 -0b101,而不是补码字符串。要查看 32 位的位模式,请先加掩码:-5 & 0xFFFFFFFF。
  • Python 中负数的右移是算术右移:-8 >> 1 是 -4,-1 >> 10 仍是 -1。
  • 定宽语言中的溢出:在 Java 或 C 中,对 32 位 int 做 1 << 31 会变成负数或未定义行为;请使用 64 位类型,并写成 1L << 40 或 1ULL << 40。
  • 混淆逻辑运算符与按位运算符:and 和 or(C 系语言中的 &&、||)返回真值,而 & 和 | 按位工作。
  • 可读性:把 x * 2 换成 x << 1,在现代编译器或 Python 中都不会更快,反而让代码更难读。

实际应用

  • Unix 文件权限:chmod 754 设置三组各 3 位的权限(rwx = 4 + 2 + 1)。
  • 网络:IPv4 子网掩码,ip & mask 可得到 CIDR 地址块的网络地址。
  • 标志与选项:Python 的 re.IGNORECASE | re.MULTILINE 和 enum.Flag 都用“或”来组合选项。
  • 图形与颜色:24 位 RGB 值把红、绿、蓝打包成 (r << 16) | (g << 8) | b。
  • 数据结构与算法:布隆过滤器、位集、树状数组(使用 i & -i)、哈希与校验和。
  • 面试题:只出现一次的数字、比特位计数、2 的幂、颠倒二进制位、丢失的数字、用位掩码枚举子集、不使用 + 求两整数之和。

常见问题

x & (x - 1) 有什么作用?

它会清除 x 最低位的 1。常用于统计 1 的个数,也可判断正整数 x 是否为 2 的幂,结果为 0 即成立。

为什么 Python 中 ~5 等于 -6?

整数使用补码,其中 ~x 等于 -x - 1。Python 把这一规则用在它没有大小上限的整数上,所以翻转 5 的每一位得到 -6。

在 Python 中如何统计一个数的二进制中 1 的个数?

Python 3.10 及以上使用 x.bit_count(),更早的版本使用 bin(x).count("1")。对于负数,bit_count() 统计的是其绝对值的位。

位运算比算术运算更快吗?

对固定宽度的整数,位运算是单条指令,但经过编译器优化后,加法和乘以常数也是。位运算真正的好处是存储紧凑和常数空间的算法,而不是取代普通的算术运算。

全部编码教程