☰
位运算技巧:解决数组中只出现一次的数字问题
2026/11/2 15:54:28 网站建设 项目流程

1. 问题背景与核心思路

这道LeetCode中等难度题目要求找出数组中唯一一个只出现一次的数字,其他数字都恰好出现三次。常规解法可以用哈希表统计频率,但题目明确要求时间复杂度O(n)且不使用额外空间,这就必须用到位运算这种"黑科技"了。

我第一次看到这个限制条件时也懵了几分钟——不用哈希表怎么统计频率?后来发现位运算的精妙之处在于:它不直接记录数字本身,而是通过二进制位的叠加状态来间接判断出现次数。就像用三盏LED灯表示0-7的数字,虽然灯本身不存储具体数值,但通过灯光的组合模式就能反推出原始数据。

2. 位运算状态机原理

2.1 二进制位独立处理

关键突破点是意识到每个二进制位可以独立计算。对于32位整数,我们创建32个计数器,分别统计每个bit位上1出现的次数。由于其他数字都出现三次,所以每个bit位的1次数要么是3的倍数(该位最终为0),要么多出1次(该位最终为1)。

这就像有32个并行的投票箱,每个箱子只记录自己那一位的投票数。最终统计时,我们只看哪些箱子的票数不是3的倍数。

2.2 状态机设计

更精妙的解法是用有限状态机表示位的变化规律。定义两个掩码变量ones和twos:

  • ones:记录出现1次的bit位
  • twos:记录出现2次的bit位

状态转移规则:

  1. 当某bit第一次出现1时,标记到ones中
  2. 当该bit第二次出现1时,从ones移除并标记到twos
  3. 当第三次出现时,从twos中移除

具体实现时,通过以下位运算完成状态转移:

ones = (ones ^ num) & ~twos twos = (twos ^ num) & ~ones

这里的& ~twos确保只在twos未置位时更新ones,而& ~ones同理。这种写法就像两个互相制约的开关,确保状态不会跳过某个步骤。

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

def singleNumber(nums): ones, twos = 0, 0 for num in nums: # 第一步:更新ones,排除已经出现两次的位 ones = (ones ^ num) & ~twos # 第二步:更新twos,排除已经出现一次的位 twos = (twos ^ num) & ~ones # 调试打印(实际提交时删除) print(f"num={num:08b}, ones={ones:08b}, twos={twos:08b}") return ones

以输入[2,2,3,2]为例(假设4位二进制):

num=0010, ones=0010, twos=0000 num=0010, ones=0000, twos=0010 num=0011, ones=0001, twos=0010 num=0010, ones=0001, twos=0000

最终ones保留的就是只出现一次的3(0011)中独有的1位。

4. 关键技巧与边界处理

4.1 运算符优先级陷阱

位运算的优先级经常让人踩坑。比如写ones ^ num & ~twos就错了,因为按位与&的优先级高于异或^。必须加括号确保执行顺序正确。

4.2 负数处理

Python的整数是补码表示且有无限位数,所以对于负数测试用例也能正确处理。但在某些语言中可能需要额外处理符号位。

4.3 通用解法扩展

如果题目改为"其他数字出现k次",可以用模k的思路。例如k=4时:

ones = (ones ^ num) & ~twos & ~threes twos = (twos ^ num) & ~ones & ~threes threes = (threes ^ num) & ~ones & ~twos

5. 性能分析与优化

时间复杂度O(32n)即O(n),空间复杂度O(1)。实测在LeetCode上运行时间能击败98%的提交。进一步优化可以考虑:

  • 提前终止:当ones和twos都归零时可以提前结束循环
  • 并行计算:使用SIMD指令同时处理多个bit位(但Python中难以实现)

6. 同类题目推荐

  1. LeetCode 136(基础版,其他数字出现两次)
  2. LeetCode 260(有两个单独数字)
  3. 《剑指Offer》56-I(数组数字出现的次数)
  4. LintCode 83(三数变形题)

这种位运算解法在硬件编程、密码学等领域都有实际应用。比如CRC校验就是基于类似的位状态转移原理。掌握后你会发现很多看似复杂的问题,其实都可以用简单的位操作优雅解决。

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

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

立即咨询