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位
状态转移规则:
- 当某bit第一次出现1时,标记到ones中
- 当该bit第二次出现1时,从ones移除并标记到twos
- 当第三次出现时,从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 & ~twos5. 性能分析与优化
时间复杂度O(32n)即O(n),空间复杂度O(1)。实测在LeetCode上运行时间能击败98%的提交。进一步优化可以考虑:
- 提前终止:当ones和twos都归零时可以提前结束循环
- 并行计算:使用SIMD指令同时处理多个bit位(但Python中难以实现)
6. 同类题目推荐
- LeetCode 136(基础版,其他数字出现两次)
- LeetCode 260(有两个单独数字)
- 《剑指Offer》56-I(数组数字出现的次数)
- LintCode 83(三数变形题)
这种位运算解法在硬件编程、密码学等领域都有实际应用。比如CRC校验就是基于类似的位状态转移原理。掌握后你会发现很多看似复杂的问题,其实都可以用简单的位操作优雅解决。