异或仅支持模2抵消,而“出现三次”需模3抵消;故需用ones/twos模拟三进制状态机,每位独立统计并清零模3余0的状态,最终ones即为答案。

为什么不能直接用异或
异或的 ^ 操作只对“出现两次抵消、奇数次保留”有效,比如 a ^ a = 0,a ^ a ^ a = a。但题目是“其他数字都出现三次”,a ^ a ^ a 结果还是 a,没法归零,所以纯异或行不通。
本质是需要一种能“模 3 抵消”的位运算机制——每个 bit 位独立统计出现次数,模 3 后剩 1 就说明目标数在该位为 1。
用两个整数模拟三进制状态机
每位有三种状态:0 次、1 次、2 次(3 次就变回 0)。用两个变量 ones 和 twos 分别记录“当前位处于状态 1”和“当前位处于状态 2”的集合。每次读入一个数 x,按以下逻辑更新:
-
twos |= ones & x:如果某位已在ones中(即已出现 1 次),又在x中为 1,则该位升为状态 2,记入twos -
ones ^= x:先无条件异或,模拟“加 1 次”(0→1 或 1→0 或 2→?) -
mask = ~(ones & twos):当某位在ones和twos同时为 1,说明实际出现了 3 次,要清零 -
ones &= mask; twos &= mask:用掩码把“溢出”的位清掉
最终答案就是 ones —— 因为只有目标数会让某位停留在状态 1,其余数都因三次出现被清零。
立即学习“C++免费学习笔记(深入)”;
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0;
for (int x : nums) {
twos |= ones & x;
ones ^= x;
int mask = ~(ones & twos);
ones &= mask;
twos &= mask;
}
return ones;
}
位运算细节容易错在哪
顺序不能调换:twos 更新必须在 ones ^= x 之前,否则 ones & x 用的是新值,状态转移就乱了。
清零掩码必须用 ~(ones & twos),不是 ~ones & ~twos —— 前者只在两者同为 1 时屏蔽,后者会误清状态 1 或状态 2 的合法位。
所有操作都是按位进行的,不依赖符号位;对负数也成立,因为补码下位运算是统一的。
有没有更直观但稍慢的办法
可以遍历 0–31 位,统计所有数在该位上 1 的个数,再对 3 取余,结果为 1 就把该位设到答案里:
int singleNumber(vector<int>& nums) {
int res = 0;
for (int i = 0; i < 32; ++i) {
int cnt = 0;
for (int x : nums) cnt += (x >> i) & 1;
if (cnt % 3) res |= (1 << i);
}
return res;
}
时间复杂度 O(32n),空间 O(1),逻辑清晰不易错,适合调试或数据量不大时用。但要注意右移对负数的行为在 C++ 中是实现定义的,稳妥起见可转为无符号右移:(static_cast<unsigned int>(x) >> i) & 1。
状态机法快且优雅,但位操作顺序和掩码逻辑一旦写反,几乎无法靠肉眼 debug,建议先手推 3 个相同数 + 1 个不同数的小例子验证。


















