⚡ 位运算技巧详解

异或、掩码、lowbit:面试高频的常数级优化与巧解

一句话总结

位运算直接操作整数的二进制位,是唯一能达到 O(1) 且常数极小的运算。面试中它主要用于三类场景:异或的性质(消消乐)、n & (n-1)(清掉最低位的 1)、掩码与位移(状态压缩)。掌握十来个固定套路即可覆盖绝大多数位运算题。

⊕

异或 ^

a^a=0、a^0=a、满足交换律结合律 → "成对消除"。

🎯

n & (n-1)

把 n 的二进制中最低位的 1 清零,用于数 1 的个数、判 2 的幂。

🔻

n & (-n)

lowbit:取出最低位的 1 所对应的值,树状数组的基础。

🎭

掩码 mask

用一个整数的每一位表示一个布尔状态,状态压缩 DP。

一、基础运算符回顾

运算符名称规则(逐位)典型用途
&按位与都为 1 才 1取指定位、清零、判奇偶 n&1
|按位或有 1 就 1设置某位为 1
^按位异或不同为 1消去成对元素、不借位加法
~按位取反0↔1构造掩码(注意符号位)
<<左移整体左移,低位补 0× 2ᵏ
>>右移整体右移(算术右移补符号位)÷ 2ᵏ(向下取整)
异或的三个黄金性质(a ^ b ^ b = a 演示) a = 1 0 1 1 0 b = 0 1 1 0 0 a^b = 1 1 0 1 0 b = 0 1 1 0 0 a^b^b = 1 0 1 1 0 == 原来的 a ✓ 性质 1:a ^ a = 0 性质 2:a ^ 0 = a 性质 3:交换律、结合律 → 一堆数异或,成对出现的自动抵消 留下来的就是"落单"的那个

二、高频技巧清单

2.1 判断奇偶 / 乘除 2 的幂

x & 1 # == 1 为奇数,== 0 为偶数(比 x % 2 快) x << 1 # x * 2 x >> 1 # x // 2(向下取整) x << k # x * 2^k

2.2 交换两个数(不用临时变量)

a ^= b # a = a ^ b b ^= a # b = b ^ (a ^ b) = 原来的 a a ^= b # a = (a ^ b) ^ a = 原来的 b # ⚠️ 注意:a 和 b 是同一个变量时会变成 0!面试常问但工程上不推荐

2.3 操作第 k 位(从 0 开始,从右往左数)

x | (1 << k) # 把第 k 位设为 1 x & ~(1 << k) # 把第 k 位清为 0 x ^ (1 << k) # 翻转第 k 位(0→1, 1→0) (x >> k) & 1 # 取出第 k 位的值 x & ((1 << k) - 1) # 取 x 的低 k 位

2.4 n & (n-1):清掉最低位的 1

n & (n-1) 的效果 n = 1 0 1 1 0 0 n-1 = 1 0 1 0 1 1 n&(n-1)= 1 0 1 0 0 0 n-1 让最低位的 1 变成 0,其右边全变 1 相与后:最低位的那个 1 被抹掉,高位不变 每执行一次就消掉一个 1 → 执行几次就有几个 1(Brian Kernighan 算法)
# 数二进制中 1 的个数(191 位 1 的个数) def hammingWeight(n): cnt = 0 while n: n &= n - 1 # 每次抹掉最低位的 1 cnt += 1 return cnt # 循环次数 = 1 的个数,比逐位检查 32 次更快 # 判断是否是 2 的幂(231):2 的幂只有一位是 1 def isPowerOfTwo(n): return n > 0 and (n & (n - 1)) == 0 # 判断是否是 4 的幂(342):先满足 2 的幂,再检查 1 在奇数位 def isPowerOfFour(n): return n > 0 and (n & (n - 1)) == 0 and (n & 0x55555555) != 0

2.5 lowbit:n & (-n)

💡 原理:计算机中 -n 用补码表示 = ~n + 1。这让 n 最低位的 1 及其右侧保持不变,其左侧全部取反。相与后,只剩下最低位的那个 1。
用途:树状数组(Fenwick Tree)的索引推进、统计 1 的个数、求最低位 1 的位置。
def lowbit(n): return n & -n # 如 n = 12 (1100) → 4 (0100) # 用 lowbit 数 1 的个数 def count_ones(n): cnt = 0 while n: n -= n & -n # 减掉最低位的 1 cnt += 1 return cnt

三、经典面试题套路

3.1 只出现一次的数字(136)

def singleNumber(nums): ans = 0 for x in nums: ans ^= x # 成对的全部抵消,剩下的就是落单的 return ans # 复杂度 O(n) 时间、O(1) 空间 —— 不用哈希表

3.2 只出现一次的数字 II(137):每个位单独统计

def singleNumber(nums): ans = 0 for k in range(32): # 逐位统计 cnt = sum((x >> k) & 1 for x in nums) if cnt % 3: # 出现 3 次的会被 % 3 消掉 ans |= 1 << k # 处理负数:Python 整数无限长,需转回有符号 32 位 return ans - (1 << 32) if ans >= (1 << 31) else ans

3.3 只出现一次的数字 III(260):分组异或

def singleNumber(nums): xor_all = 0 for x in nums: xor_all ^= x # = a ^ b(两个落单数) # 取出 a^b 最低位的 1 —— 说明 a 和 b 在这一位上不同 diff = xor_all & -xor_all a = b = 0 for x in nums: if x & diff: # 按这一位是否为 1 分成两组 a ^= x # 每组里成对的仍会抵消,各剩一个落单数 else: b ^= x return [a, b]

3.4 不用加减号做加法(371 / 剑指 Offer 65)

def add(a, b): while b: carry = (a & b) << 1 # 进位:只有两位都是 1 才产生进位 a = a ^ b # 不进位相加:异或就是"半加" b = carry # 把进位再加回去,反复直到进位为 0 return a # 剑指 Offer 65(Python 需处理 32 位溢出与负数) def add_32(a, b): MASK = 0xFFFFFFFF a, b = a & MASK, b & MASK while b: a, b = (a ^ b) & MASK, ((a & b) << 1) & MASK return a if a <= 0x7FFFFFFF else ~ (a ^ MASK)

3.5 缺失的数字 / 找不同(268 / 389)

# 268 缺失的数字:0..n 中缺一个 → 把下标和值一起异或 def missingNumber(nums): ans = len(nums) for i, x in enumerate(nums): ans ^= i ^ x # 成对抵消,缺的那个会留下来 return ans # 389 找不同:s 比 t 多一个字符 def findTheDifference(s, t): ans = 0 for c in s + t: ans ^= ord(c) return chr(ans)

四、状态压缩

4.1 用一个整数表示集合

当元素个数 ≤ 20~25 时,可以用一个整数的每一位表示"第 i 个元素是否被选中",从而把集合状态压缩成一个 int,配合 DP 使用。

S = 0 # 空集合 S |= 1 << i # 加入元素 i S &= ~(1 << i) # 移除元素 i (S >> i) & 1 # 判断 i 是否在集合中 S & (1 << i) # 同上(非 0 即存在) S1 | S2 # 并集 S1 & S2 # 交集 bin(S).count('1') # 集合大小(1 的个数) # 枚举 S 的所有非空子集(经典技巧) sub = S while sub: # 处理子集 sub sub = (sub - 1) & S # 下一个子集

4.2 应用题目

题目思路
78 子集用 mask 从 0 到 2ⁿ-1 枚举所有子集
698 / 473 划分为 k 个相等的子集状态压缩 DP + 记忆化
847 访问所有节点的最短路径BFS + 状态压缩(当前节点, 已访问集合)
943 最短超级串状压 DP:dp[mask][i]
691 贴纸拼词状压 DP:用 mask 表示已拼出的字符

五、注意事项与陷阱

⚠️ 常见坑:
① 运算符优先级:&、^、| 的优先级低于 == 和比较运算符。写 if n & 1 == 1 实际会被解析成 if n & (1 == 1)!必须加括号:if (n & 1) == 1。
② Python 整数无限精度:负数右移会无限补符号位,~n 的结果和 C/Java 不同。处理 32 位题时需手动 & 0xFFFFFFFF 掩码。
③ 位移位数超限:C/C++ 中位移 ≥ 字长属未定义行为。
④ 可读性:a ^= b; b ^= a; a ^= b 交换法在 a、b 同址时会清零,且现代编译器下并不比临时变量快,工程代码不推荐。
记忆口诀:
成对消除用异或;
数 1、判 2 的幂用 n & (n-1);
取最低位 1 用 n & -n;
设位/清位/翻位用 1 << k 配合 | &~ ^;
集合状态压成 int,枚举子集用 (sub-1) & S。