☰
LeetCode 137题位运算解法精讲:统计二进制位技巧
2026/11/2 15:54:32 网站建设 项目流程

1. 问题背景与核心挑战

LeetCode 137题"只出现一次的数字 II"是位运算领域的经典问题。题目给定一个整数数组,其中某个元素只出现一次,其余每个元素均出现三次,要求找出那个单一的数字。这个问题的常规解法可能包括哈希表统计或数学计算,但位运算解法才是真正考验算法功底的方案。

我第一次遇到这个问题时,尝试用哈希表记录每个数字的出现次数。虽然这种方法时间复杂度是O(n),但空间复杂度也是O(n),不符合题目进阶要求中"常数空间复杂度"的提示。这促使我深入研究位运算的解法,发现其中蕴含着精妙的二进制位操作逻辑。

2. 位运算解法原理剖析

2.1 二进制位统计思想

位运算解法的核心思路是:统计所有数字在各个二进制位上1出现的次数。由于除了目标数字外,其他数字都出现三次,因此每个位上1的总次数应该是3的倍数(可能加上目标数字在该位的1)。

具体来说,对于32位整数中的每一位:

  1. 初始化一个长度为32的数组count
  2. 遍历所有数字,统计每个数字在每一位上是否为1
  3. 最后检查count数组,如果某位的计数不是3的倍数,说明目标数字在该位是1

2.2 位运算优化技巧

直接使用数组统计虽然直观,但空间效率不高。更优雅的做法是用两个整数变量ones和twos来记录状态:

  • ones表示某位出现1的次数是否为1次(模3余1)
  • twos表示某位出现1的次数是否为2次(模3余2)

每次处理新数字时,通过位运算更新这两个变量的状态:

  1. ones = (ones ^ num) & ~twos
  2. twos = (twos ^ num) & ~ones

这种状态机的设计非常巧妙,通过异或和与运算实现了模3计数器。

3. 完整代码实现与逐行解析

3.1 Python实现版本

def singleNumber(nums): ones, twos = 0, 0 for num in nums: ones = (ones ^ num) & ~twos twos = (twos ^ num) & ~ones return ones

代码解析:

  1. 初始化ones和twos为0
  2. 遍历数组中的每个数字num
  3. 更新ones:先用异或记录出现奇数次的位,再用与运算确保该位不在twos中
  4. 更新twos:同样先用异或记录,再确保该位不在ones中
  5. 最终ones保存的就是只出现一次的数字

3.2 状态转移过程示例

以输入[2,2,3,2]为例:

  1. 初始状态:ones=0, twos=0
  2. 处理2:
    • ones = (0^2)&~0 = 2
    • twos = (0^2)&~2 = 0
  3. 处理2:
    • ones = (2^2)&~0 = 0
    • twos = (0^2)&~0 = 2
  4. 处理2:
    • ones = (0^2)&~2 = 0
    • twos = (2^2)&~0 = 0
  5. 处理3:
    • ones = (0^3)&~0 = 3
    • twos = (0^3)&~3 = 0 最终返回ones=3

4. 算法复杂度与边界条件

4.1 时间复杂度分析

该算法只需要一次线性扫描数组,时间复杂度为O(n),其中n是数组长度。对于每个数字的处理都是常数时间操作(32位整数的位运算)。

4.2 空间复杂度优势

仅使用了两个额外整数变量ones和twos,空间复杂度是O(1),远优于哈希表解法的O(n)。

4.3 边界条件处理

需要注意的特殊情况包括:

  1. 空数组:题目保证有解,实际可以不做处理
  2. 最大/最小整数:32位整数范围是-2^31到2^31-1,算法自动处理
  3. 所有数字都出现三次:题目保证有唯一解

5. 位运算技巧扩展应用

5.1 通用解法框架

这类问题可以抽象为"找出在n次重复中出现一次的数字"。对于一般情况,我们可以设计一个状态机,用k个变量来记录模n的余数状态。

5.2 类似题目推荐

  1. LeetCode 136:只出现一次的数字(其他数字出现两次)
  2. LeetCode 260:只出现一次的数字 III
  3. LeetCode 477:所有汉明距离总和(位统计思想)

5.3 位运算优化技巧

在实际编码竞赛中,位运算技巧还包括:

  1. 快速判断奇偶:num & 1
  2. 交换两个数:a ^= b; b ^= a; a ^= b
  3. 取绝对值:(num ^ (num >> 31)) - (num >> 31)

6. 常见错误与调试技巧

6.1 典型错误模式

  1. 混淆ones和twos的更新顺序:必须先更新ones再更新twos
  2. 忽略负数的二进制表示:Python使用补码表示,但算法自动处理
  3. 错误初始化:ones和twos必须初始化为0

6.2 调试建议

  1. 打印中间状态:在处理每个数字后打印ones和twos的值
  2. 小规模测试:先用简单例子验证(如[1,1,1,2])
  3. 边界测试:测试最大/最小整数和零的情况

7. 性能优化与替代方案

7.1 位统计法的变体

另一种思路是分别计算每一位上1的个数,然后模3:

def singleNumber(nums): result = 0 for i in range(32): mask = 1 << i count = 0 for num in nums: if num & mask: count += 1 if count % 3: result |= mask return result if result < (1 << 31) else result - (1 << 32)

这种方法更直观但效率稍低(32次循环)。

7.2 数学方法

利用集合和数学计算:

  1. sum(set(nums))*3 - sum(nums) 然后除以2 但需要注意整数溢出问题。

8. 实际应用场景

这类位运算技巧在以下场景有实际应用:

  1. 错误检测与纠正:奇偶校验位
  2. 数据压缩:位级编码
  3. 密码学:位操作是许多加密算法的基础
  4. 硬件编程:直接操作寄存器位

我在实际工作中曾用类似技巧处理过设备状态寄存器,需要从多个重复状态信号中提取异常事件。位运算解法的高效性在这种底层系统编程中尤为重要。

9. 学习路径建议

对于想精通位运算的开发者,我建议的学习顺序:

  1. 掌握基本位操作:与、或、非、异或、移位
  2. 理解补码表示:特别是负数的二进制形式
  3. 练习简单题目:如136题(异或技巧)
  4. 挑战中等难度:如本问题和260题
  5. 研究高级应用:布隆过滤器、位图等

10. 个人实战心得

经过多次练习这类题目,我总结了几个关键点:

  1. 画状态转移图有助于理解ones/twos的更新逻辑
  2. 对于复杂位运算,先写注释再写代码
  3. 测试时要包含正数、负数和零的混合情况
  4. 在竞赛中,这类技巧往往能大幅提升性能

最后分享一个小技巧:当不确定位运算结果时,可以先用bin()函数打印数字的二进制表示,这在调试时非常有用。例如print(bin(ones), bin(twos))可以清晰看到状态变化过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询