基础篇:二进制与位运算基础
1.1 为什么需要位运算
在现代计算机体系结构中,数据以二进制(0和1)的形式存储和运算。位运算直接操作内存中的比特位,具有以下无可比拟的优势:
-
极致的速度:位运算对应CPU指令集(如AND, OR, XOR, SHIFT)的单个指令周期,比加减乘除快数倍甚至数十倍。
-
极低的内存占用:利用位掩码(Bitmask),一个int(32位)可以表示32种布尔状态,常用于状态压缩DP。
-
代码优雅性:某些复杂逻辑(如权限判断、奇偶性、2的幂判断)用位运算表达极其简洁。
-
硬件控制基础:嵌入式、驱动开发、网络协议栈(TCP/IP头)、加密算法(AES、RSA)的核心均依赖位运算。
1.2 二进制表示与原码、反码、补码
在深入位运算前,必须明确计算机中整数的存储方式。
-
原码:最高位为符号位(0正1负),其余位表示数值。
-
例如:+5 = 0000 0101,-5 = 1000 0101。
-
缺点:0有两种表示(0000 0000和1000 0000),且减法运算复杂。
-
-
反码:正数不变,负数符号位不变,其余位取反。
-
-5反码 = 1111 1010。
-
缺点:0仍有两种表示,且加减运算需循环进位。
-
-
补码:现代计算机唯一采用的整数编码。
-
正数:原码相同。
-
负数:反码 +1。
-
-5补码 = 1111 1011。
-
优点:
-
统一加减法:A – B = A + (~B + 1),减法器可复用加法器。
-
唯一零:-0的补码表示为1 0000 0000,超出范围自动丢弃,最终0000 0000。
-
范围对称:int8_t范围是-128 ~ 127,负数比正数多一个。
-
关键注意:在C/C++/Java中,有符号整数右移(>>)是算术右移(补符号位),无符号整数右移(>>>在Java中)是逻辑右移(补0)。Python的整数是无限精度的,右移模拟算术右移。
1.3 六大基础位运算符
假设A = 60 (二进制 0011 1100),B = 13 (二进制 0000 1101)。
| & | 按位与 | 两位同时为1,结果为1 | 0000 1100 (12) |
| | | 按位或 | 两位只要有一个为1,结果为1 | 0011 1101 (61) |
| ^ | 按位异或 | 两位不同为1,相同为0 | 0011 0001 (49) |
| ~ | 按位取反 | 单目运算符,0变1,1变0 | 1100 0011 (-61) |
| << | 左移 | 高位丢弃,低位补0,相当于乘以2 | A << 2 = 1111 0000 (240) |
| >> | 右移 | 对于正数,高位补0;负数依赖编译器 | A >> 2 = 0000 1111 (15) |
1.3.1 按位与 (&)
-
特性:X & 1 = X (保留最低位),X & 0 = 0。
-
用途:清零特定位、取指定位、判断奇偶。
1.3.2 按位或 (|)
-
特性:X | 1 = 1 (强制设为1),X | 0 = X。
-
用途:将某些位设为1。
1.3.3 按位异或 (^)
-
特性:
-
X ^ 0 = X
-
X ^ X = 0
-
X ^ Y ^ Y = X (自反性)
-
满足交换律和结合律。
-
-
用途:无临时变量交换、寻找单身数字、简单的加密解密。
1.3.4 左移 (<<) 与 右移 (>>)
-
左移:相当于乘以2^n,但需注意溢出风险(C/C++中左移溢出未定义行为)。
-
右移:相当于除以2^n并向下取整(针对正数)。对于负数,不同语言实现不同。
1.4 位运算的优先级与常见误区
位运算的优先级通常低于算术运算符(+、-、*、/),但高于比较运算符(==、!=)和逻辑运算符(&&、||)。
常见错误:
c
// 错误:期望判断 (a & b) == 0,但实际解析为 a & (b == 0)
if (a & b == 0) { … }
// 正确:
if ((a & b) == 0) { … }
建议:在涉及混合运算时,使用括号明确优先级,不仅避免错误,也提高可读性。
进阶篇:经典位运算技巧
2.1 判断奇偶性
传统方法:n % 2 == 0。
位运算方法:(n & 1) == 0 为偶数,(n & 1) == 1 为奇数。
-
原理:二进制最低位(LSB)为1则为奇数,为0则为偶数。
-
性能:取模运算通常较慢,位运算快一个数量级。
2.2 交换两数(不使用临时变量)
c
int a = 5, b = 3;
a = a ^ b; // a = 5^3
b = a ^ b; // b = (5^3)^3 = 5
a = a ^ b; // a = (5^3)^5 = 3
-
原理:利用异或的自反性 a ^ b ^ b = a。
-
注意:当 a 和 b 指向同一内存地址时(如 swap(&a, &a)),会出错(值被清零)。在工程代码中,除非极度追求性能,否则不推荐使用(可读性差),但在算法面试中常出现。
2.3 消去二进制中的最后一个1 (x & (x-1))
这是最强大的技巧之一,用于将二进制中最右边的1变为0。
-
示例:x = 12 (1100),x-1 = 11 (1011),x & (x-1) = 1000 (8)。
-
应用:
-
统计二进制中1的个数:循环执行直到x变为0。
-
判断2的幂:如果 x & (x-1) == 0 且 x != 0,则 x 是2的幂。
-
计算x是2的多少次幂的变体:如求x的二进制表示中最低位1的权值。
2.4 获取最低位的1 (x & -x)
-
原理:-x在补码下等于~x + 1。x & -x 得到的结果是 x 的最低位1所代表的数值。
-
示例:x = 12 (1100),-x = 0100 (补码),1100 & 0100 = 0100 (4)。
-
应用:树状数组(Fenwick Tree)的lowbit函数、提取集合中的最小元素。
2.5 位掩码(Bitmask)与状态压缩
位掩码是利用一个整数的二进制位来表示集合状态的技术。
-
基本操作:
-
空集:mask = 0
-
添加元素i:mask |= (1 << i)
-
删除元素i:mask &= ~(1 << i)
-
检查元素i:(mask >> i) & 1 或 mask & (1 << i)
-
切换元素i:mask ^= (1 << i)
-
集合大小:__builtin_popcount(mask) (GCC) 或 Integer.bitCount(mask) (Java)
-
-
状态压缩DP:当状态维度较少(通常n <= 20)时,可以将每个元素“选/不选”的状态压缩成一个整数,作为DP的维度。
-
经典问题:TSP问题(旅行商问题),DP[mask][u] 表示访问过 mask 集合中的城市,最后在 u 的最短路径。
-
2.6 异或运算的四大定律与应用
异或是位运算中最具数学美感的操作,遵循以下定律:
归零律:a ^ a = 0
恒等律:a ^ 0 = a
交换律:a ^ b = b ^ a
结合律:(a ^ b) ^ c = a ^ (b ^ c)
衍生应用:
-
出现奇数次的数字:在一个数组中,只有一个数字出现奇数次,其余均出现偶数次,求该数字。遍历异或即可。
-
无临时变量交换:已介绍。
-
格雷码(Gray Code):相邻两个数的二进制只有一位不同。生成公式:n ^ (n >> 1)。
-
简单的对称加密:
-
加密:cipher = plain ^ key
-
解密:plain = cipher ^ key
-
实战篇:LeetCode高频题解
3.1 基础题型
136. 只出现一次的数字
题目:给定一个非空整数数组,除了某个元素只出现一次外,其余每个元素均出现两次。找出那个只出现一次的元素。
思路:利用异或的归零律和恒等律。遍历所有数字,进行异或运算,最终结果即为落单的数字。
python
def singleNumber(nums):
res = 0
for num in nums:
res ^= num
return res
复杂度:时间O(n),空间O(1)。
191. 位1的个数
题目:编写一个函数,输入是一个无符号整数,返回其二进制表示中数字位数为 '1' 的个数。
解法一:逐位检查(右移)
python
def hammingWeight(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
解法二:n & (n-1) 优化(跳过0位)
python
def hammingWeight(n):
count = 0
while n:
n &= (n – 1) # 消去最低位的1
count += 1
return count
内置方法:bin(n).count('1') 或 n.bit_count() (Python 3.8+)。
231. 2的幂
题目:给定一个整数,编写一个函数来判断它是否是 2 的幂次方。
思路:
负数、0 排除。
2的幂的二进制只有一个1,即 n & (n-1) == 0。
python
def isPowerOfTwo(n):
return n > 0 and (n & (n – 1)) == 0
3.2 中等题型
260. 只出现一次的数字 III
题目:给定一个整数数组,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。
思路:
全体异或得到 diff = a ^ b。
diff 中为1的位表示 a 和 b 在该位不同。
利用 diff & -diff 提取最低位的不同位 mask。
根据该位将原数组分为两组,分别异或,得到 a 和 b。
python
def singleNumber(nums):
diff = 0
for num in nums:
diff ^= num
# 获取最低位的1
mask = diff & -diff
a, b = 0, 0
for num in nums:
if num & mask:
a ^= num
else:
b ^= num
return [a, b]
338. 比特位计数
题目:给定一个非负整数 n,对于 0 ≤ i ≤ n,计算每个数字二进制中 1 的个数,返回一个数组。
思路:动态规划 + 位运算。
-
DP 公式:dp[i] = dp[i >> 1] + (i & 1)。
-
i >> 1 相当于去掉最低位,(i & 1) 是判断最低位是否为1。
-
-
DP 公式二:dp[i] = dp[i & (i-1)] + 1。
python
def countBits(n):
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1)
return dp
371. 两整数之和
题目:不使用运算符 + 和 -,计算两整数之和。
思路:利用位运算模拟加法器。
不进位加法:a ^ b 得到忽略进位的和。
进位:(a & b) << 1 得到进位。
递归/迭代,直到进位为0。
-
注意:Python 整数无溢出,需要模拟32位有符号整数的截断(& 0xFFFFFFFF)。
python
def getSum(a, b):
# 32位掩码
mask = 0xFFFFFFFF
while b != 0:
carry = (a & b) & mask
a = (a ^ b) & mask
b = (carry << 1) & mask
# 如果a是负数(超过32位有符号范围),转换
return a if a <= 0x7FFFFFFF else ~(a ^ mask)
3.3 困难题型
85. 最大矩形(结合位运算优化)
题目:给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
常规解法:柱状图法(单调栈),复杂度 O(m * n)。
位运算优化解法:
将矩阵每一行视为二进制数 row_mask。
枚举矩形的上边界 i,向下累加(按位与)得到 current_mask。
对于每个 current_mask,计算其中连续的1的最大长度(即 current_mask 中连续1的个数)。
-
利用 while current_mask 和 current_mask & (current_mask << 1) 技巧快速计算宽度。
-
虽然理论复杂度仍是 O(m * n),但常数极小,且适合位并行计算。
982. 按位与为零的三元组
题目:给定一个整数数组 A,找出满足 (A[i] & A[j] & A[k]) == 0 的三元组个数。
思路:
暴力法:O(n^3) 不可行。
计数优化:
-
先枚举所有 i, j,统计 A[i] & A[j] 的频次,存入 freq。
-
再枚举每个 k,遍历所有可能的 mask(最多2^16=65536种),判断 (mask & A[k]) == 0,累加频次。
子集枚举优化:对于给定的 val = A[k],需要找所有 mask 使得 mask & val == 0,即 mask 是 ~val 的子集。利用子集枚举法(sub = (sub – 1) & complement)遍历。
python
def countTriplets(A):
freq = [0] * (1 << 16)
n = len(A)
for i in range(n):
for j in range(n):
freq[A[i] & A[j]] += 1
res = 0
for k in range(n):
val = A[k]
# 枚举 complement 的子集
complement = (~val) & ((1 << 16) – 1)
sub = complement
while True:
res += freq[sub]
if sub == 0:
break
sub = (sub – 1) & complement
return res
高阶篇:系统底层与工程应用
4.1 快速幂与快速乘法
快速幂(Exponentiation by squaring)利用二进制分解指数,将 O(n) 的乘法次数降至 O(log n)。
-
快速幂(模意义下):
python
def pow_mod(a, b, mod):
res = 1
base = a % mod
while b > 0:
if b & 1: # 如果当前二进制位是1
res = (res * base) % mod
base = (base * base) % mod
b >>= 1
return res
-
快速乘(防止溢出):类似原理,将加法代替乘法,用于大数乘法取模。
4.2 布隆过滤器(Bloom Filter)
布隆过滤器是一种空间效率极高的概率性数据结构,用于判断一个元素是否在集合中。
原理:
初始化一个位数组(Bit Array),长度为 m。
使用 k 个独立的哈希函数。
添加:对元素计算 k 个哈希值,将位数组中对应的 k 个位置设为 1。
查询:对元素计算 k 个哈希值,如果所有对应位都是 1,则元素可能存在(存在误判率);如果任意一位是 0,则元素一定不存在。
位运算角色:布隆过滤器的底层存储就是位掩码,设置位(bits[hash] = 1)和检查位(bits[hash] & 1)均依赖位运算。
应用:防止缓存穿透(Redis)、恶意URL过滤、数据库查询优化。
4.3 内存对齐与位域(Bit Fields)
在C/C++系统编程中,位域(Bit Fields)允许开发者精确控制结构体的内存布局,按位分配存储空间。
c
struct DeviceConfig {
unsigned int enabled : 1; // 占用1位
unsigned int mode : 2; // 占用2位
unsigned int speed : 4; // 占用4位
unsigned int : 1; // 无名位域,填充对齐
};
-
作用:节省内存、直接映射硬件寄存器。
-
位运算关联:编译底层即转化为位移和掩码操作。
4.4 权限系统设计(RBAC 位运算实现)
在中小型系统的权限设计中,使用位掩码可以极简地实现权限的组合与判断。
设计:
-
定义权限枚举(2的幂):
-
CREATE = 1 << 0 (1)
-
READ = 1 << 1 (2)
-
UPDATE = 1 << 2 (4)
-
DELETE = 1 << 3 (8)
-
-
角色权限:
-
ADMIN = CREATE | READ | UPDATE | DELETE (15)
-
USER = READ (2)
-
-
判断权限:
-
if (user_permission & required_perm)
-
-
授予权限:
-
user_permission |= perm
-
-
撤销权限:
-
user_permission &= ~perm
-
优点:单字段存储,数据库查询友好,且权限组合无数量限制(32位系统最多32种权限)。
总结:位运算的思维模型
原子性思维:位运算是直接操作计算机最底层的原子单元(比特)。掌握位运算意味着能够从机器的视角理解运算本质。
集合论思维:位掩码是集合的完美编码。& 对应交集,| 对应并集,^ 对应对称差,~ 对应补集。许多集合问题可转化为位运算,极大地简化代码。
二进制分解思维:任何整数都可以分解为 2 的幂次和。快速幂、状态压缩DP、线段树等高级数据结构和算法均基于此。
时间复杂度优化:位运算通常将 O(n) 的循环操作(如计数、求反)降为 O(1) 或 O(log n),并显著降低常数因子。
工程权衡:虽然位运算性能优异,但过度使用会牺牲可读性。在实际工程中,应在“性能瓶颈处”与“算法核心处”合理使用,并辅以清晰注释。
位运算不仅是算法竞赛的“神器”,更是连接软件与硬件的桥梁。从最简单的奇偶判断,到复杂的 CPU 指令优化,再到庞大的分布式系统(如 Bloom Filter),位运算无处不在。希望这篇 2 万字的详解能帮助你建立完整的位运算知识体系,在算法学习和工程实践中游刃有余。
附录:常用位运算速查表
| 清除最低位1 | n & (n-1) | 0101 & 0100 = 0100 (4) |
| 获取最低位1 | n & -n | 0101 & 1011 = 0001 (1) |
| 将第i位设为1 | n | (1 << i) | 0101 | (1<<2) = 0101 | 0100 = 0101 (5) |
| 将第i位设为0 | n & ~(1 << i) | 0101 & ~(1<<2) = 0101 & 1011 = 0001 (1) |
| 检查第i位 | (n >> i) & 1 | (5>>2)&1 = 1 |
| 切换第i位 | n ^ (1 << i) | 0101 ^ 0100 = 0001 (1) |
| 取模2的幂 | n & (m-1) | 当m=8时,n%8 = n&7 |



