
题目
数组 nums 包含 1~N 中的数,但少了两个。找出这两个数。要求 O(N) 时间、O(1) 空间。
输入: nums = [1] → 输出: [2,3] (N=3)
输入: nums = [2,3] → 输出: [1,4] (N=4)
注意 N = nums.length + 2 ,不是 +1------这是这题最容易写错的地方。
思路:268 题 + 260 题的组合
这题本质是两道题的缝合:
| 来源 | 借鉴的思路 |
|---|---|
| 268 丢失的数字 | 让"数组里的数"和"完整序列"互相异或,成对抵消 |
| 260 只出现一次的数字 III | 用 a^b 的某个 1 作分岔位,分成两组各自异或 |
第一步:全部异或,得到 a^b
把数组里的数和 1~N 全部异或到一起:
出现过的数:在数组里有一份、在 1~N 里也有一份 → 抵消成 0
缺失的两个数:只在 1~N 里有,没有配对 → 剩下来
tmp = a ^ b
第二步:找一个"a、b 不一样"的位
tmp 里为 1 的位,说明 a 和 b 在这一位上不同。你笔记里的 while(true) 就是干这个的:
int diff = 0;
while (true) {
if (((tmp >> diff) & 1) == 1) break;
else diff++;
}
但这里推荐用你笔记里记过的 lowbit 一行替代:
int lb = tmp & -tmp; // 直接取出最右侧的那个 1
区别是 diff 得到的是位号 ,lb 得到的是掩码 。用掩码判断 (x & lb) == 0 比 (x >> diff) & 1 少一次移位,更简洁。
**为了寻找diff位置,即最右侧的1的比特位**
第三步:按这一位分组,组内各自异或
该位为 0 的一组:组内成对的抵消,只剩一个数
该位为 1 的一组:同理,只剩另一个数
完整代码(修正版)
class Solution {
public int[] missingTwo(int[] nums) {
int n = nums.length + 2; // ← 完整范围是 1~n,别写成 +1
// 第一步:全部异或,得 a^b
int tmp = 0;
for (int x : nums) tmp ^= x;
for (int i = 1; i <= n; i++) tmp ^= i; // ← 必须是 <=
// 第二步:取分岔位
int lb = tmp & -tmp;
// 第三步:分组异或
int[] ret = new int[2];
for (int x : nums) {
if ((x & lb) == 0) ret[0] ^= x;
else ret[1] ^= x;
}
for (int i = 1; i <= n; i++) { // ← 同样是 <=
if ((i & lb) == 0) ret[0] ^= i;
else ret[1] ^= i;
}
return ret;
}
}
代码里的真实长相
int[] ret = new int[2];
就这行。意思是:准备两个空盒子:
ret[0] ret[1]
┌──────┐ ┌──────┐
│ 0 │ │ 0 │ ← 都是空的(初始0)
└──────┘ └──────┘
左盒子 右盒子
(我说的"左堆") (我说的"右堆")
什么是"扔进盒子"
ret[0] ^= x; // 把 x 扔进左盒子
ret[1] ^= x; // 把 x 扔进右盒子
"扔进去"不是真的把数放进去,而是跟盒子里现有的数异或一下:
ret[1] 原来 = 0
ret[1] ^= 1 → 0 ^ 1 = 1
现在 ret[1] = 1
就像往存钱罐里投硬币------盒子只记一个"累计结果",不记每个数分别是啥。
扔的规则是什么
就是刚刚算出来的 diff:
看这个数的第
diff位是 0 还是 1
if (((x >> diff) & 1) == 1) ret[1] ^= x; // 是1 → 扔右盒子
else ret[0] ^= x; // 是0 → 扔左盒子
完整演示(nums=1,完整是1~3,diff=0)
规则:看第 0 位。
一开始: ret[0] = 0 ret[1] = 0
┌─────┐ ┌─────┐
│ 0 │ │ 0 │
└─────┘ └─────┘
处理数组的数:
x=1 第0位=1 → 扔右盒: ret[1] = 0^1 = 1
┌─────┐ ┌─────┐
│ 0 │ │ 1 │
└─────┘ └─────┘
处理范围的数:
i=1 第0位=1 → 扔右盒: ret[1] = 1^1 = 0 ← 抵消,变回0!
┌─────┐ ┌─────┐
│ 0 │ │ 0 │
└─────┘ └─────┘
i=2 第0位=0 → 扔左盒: ret[0] = 0^2 = 2
┌─────┐ ┌─────┐
│ 2 │ │ 0 │
└─────┘ └─────┘
i=3 第0位=1 → 扔右盒: ret[1] = 0^3 = 3
┌─────┐ ┌─────┐
│ 2 │ │ 3 │
└─────┘ └─────┘
最后两个盒子里剩下的:2 和 3 ------就是答案。
为什么叫"分堆"
因为每个数只看一眼第 0 位,就自动去了某个盒子,像分拣机器:
所有数
│
▼
┌─────────────┐
│ 第0位是几? │
└─────────────┘
│ │
是 0 是 1
▼ ▼
┌──────┐ ┌──────┐
│ret[0]│ │ret[1]│
│ 左堆 │ │ 右堆 │
└──────┘ └──────┘
一句话
"左堆右堆"就是
ret[0]和ret[1]这两个盒子。每个数按第diff位的值,扔进其中一个盒子(扔进去 = 跟盒子里的数异或)。最后两个盒子里各剩一个数,就是答案。
所以代码里最后 return ret; ------ 就是把这两个盒子交出去。
⚠️ 三个必踩的坑
① 循环必须是 i <= n,不能是 i < n
n = nums.length + 2 是完整范围的最大值 ,i < n 只覆盖到 n-1,把 n 漏了。
这是你刚才报错的直接原因------nums = [1] 时输出 [0,2] 而不是 [2,3]。
② N 是 nums.length + 2,不是 +1
数组少了两个 数,所以完整长度 = nums.length + 2。
③ while(true) 有理论死循环风险
如果 tmp == 0(两个缺失数相同),diff 会一直加下去。但这题不会出现------两个缺失数必然不同,a^b 一定非 0。不过用 tmp & -tmp 就没这个顾虑,更稳。
实测结果
| nums | 期望 |
|---|---|
[1] |
2,3 ✓ |
[2,3] |
1,4 ✓ |
[1,3] |
2,4 ✓ |
[4,2,1] |
3,5 ✓ |
[] |
1,2 ✓ |
(结果顺序无所谓,2,3 和 3,2 都算对)
一句话总结
全部异或让出现过的数自己和自己抵消,得到
a^b;取tmp最右侧的 1 当分岔位,把数组和完整序列都按这一位分两组,各组内部再异或一次,成对的自会抵消,各剩一个缺失数。
思维链条 :268(缺 1 个)→ 把"数组"和"完整序列"配对异或;260(缺 2 个的反向版)→ lowbit 分组。这题是两题的合成体。