260. 只出现一次的数字 III(Single Number III)

频次 ★★★ · 难度 🟡 · 高频:字节

题目

两个元素只出现一次,其余各出现两次。找出这两个元素。

思路

先全部异或得到 xor = a ^ b。取 xor 最低位的 1(xor & -xor)把数组分成两组,每组各自异或得到两个数。

代码

public int[] singleNumber(int[] nums) {
    int xor = 0;
    for (int n : nums) xor ^= n;
 
    int mask = xor & -xor;          // 最低位的 1
    int a = 0, b = 0;
    for (int n : nums) {
        if ((n & mask) == 0) a ^= n;
        else b ^= n;
    }
    return new int[]{a, b};
}

复杂度

  • 时间:O(n),两趟遍历
  • 空间:O(1)——这是本题相对哈希表计数的全部价值

边界条件

  • ab 必然不同,所以 xor = a ^ b != 0mask 一定存在。这是分组能成立的前提
  • xorInteger.MIN_VALUE-xor 溢出后仍等于自身,xor & -xor 恰好还是最高位那个 1,结果正确
  • 数组只有两个元素:第一趟 xor = a ^ b,第二趟各分一组,直接得解
  • 出现两次的元素必然落在同一组(它们的每一位都相同),组内异或自动消掉

变式

  • 136. 只出现一次的数字:只有一个落单,全体异或即可,不需要分组
  • 其余元素出现三次,一个出现一次:异或失效(三个相同数异或还剩一个)。改成按位统计每一位上 1 的个数模 3,或用「两个状态变量模拟三进制计数器」
  • 三个元素只出现一次xor 是三者异或,无法简单二分。要先找到一个必然存在的、能把三者分开的位,做法复杂得多
  • 求两个数中较小的那个:分组完拿到 a、b 后直接比

易错点

  • xor & -xor 取的是最低位的 1,不是最高位。任何一个为 1 的位都能用来分组,取最低位只是因为这个写法最短
  • 分组的判据是 (n & mask) == 0,不是 (n & mask) == 1mask 可能是 0b1000,与出来的结果是 8 不是 1
  • 第二趟遍历要走完整数组,不能只走一半——出现两次的元素也要参与异或才能被消掉
  • 不要先把数组按 mask 物理分成两个数组再异或,那会多 O(n) 空间,白白丢掉本题的核心优势

面试追问

  • x & -x 为什么能取出最低位的 1-x~x + 1。取反后最低位的 1 变成 0、其右边的 0 全变 1;再加 1 使这些位进位回来,结果是「最低位的 1 保持,其左边所有位与 x 相反」。与一下,只剩那个 1。这个式子在树状数组里叫 lowbit,是它的核心。
  • 为什么异或能消掉成对的数:异或满足交换律、结合律,且 a ^ a = 0a ^ 0 = a。所以全体异或等价于「把所有成对的消掉,剩下落单的」。异或的自反性就是一个天然的、O(1) 空间的『配对消除器』。
  • 分组的正确性怎么证mask 位上 a 和 b 必然一个是 0 一个是 1(否则该位异或为 0,不会被 xor 选中)。所以 a、b 必然分属两组;而任何出现两次的数,两次都落进同一组。于是每组恰好是「一个落单 + 若干成对」,退化成 136。
  • 有没有不用位运算的 O(1) 空间解:没有通用解。哈希表 O(n) 空间,排序 O(n log n) 时间且破坏原数组。位运算在这里不是炫技,是唯一能同时做到 O(n) 时间 + O(1) 空间的路径。

关联题