深蓝黑背景品牌封面:左侧大标题「数组里缺了两个数」,副标题「O(n) 时间、O(1) 空间找出来」;右侧原创装饰——一排标着 1 到 8 的格子,其中 3 和 6 两个格子是虚线空位,其余格子填实,空位上方有问号;左上标注「经典题解析 · 算法」,右上标注「Day 09 · 10-07」 图 1:本文封面,点明题目与复杂度约束。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题面:1 到 n+2 这些整数本该全在一个数组里,现在缺了两个,数组乱序。要求 O(n) 时间、O(1) 额外空间,把缺的两个数找出来。

面试现场,这道题先淘汰的是「排序后扫一遍」——O(n log n) 时间超标;再淘汰「开个哈希表登记出现过的数」——O(n) 空间超标。面试官想等的是:不开额外存储,怎么用「整体特征」把两个未知数挤出来。

核心结论:两条路都通,一条用数学求和,一条用位运算异或。

解法一:求和加平方和。

缺的两个数记为 a 和 b。一趟遍历算出数组的实际和,再用 1 到 n+2 的理论和减去它,得到 a + b。一个方程解不出两个未知数,那就再造一个:同样方法算平方和之差,得到 a² + b²。两个方程联立:由 (a−b)² = 2(a²+b²) − (a+b)² 求出 |a−b|,再和 a+b 一配,a、b 就都出来了。一趟遍历、两个累加器,时间与空间都达标。

这个解法唯一要警惕的是溢出。n 上到十万,平方和就是 10¹⁵ 量级,超出 32 位整型的射程;用 JavaScript 写演示时 Number 也不保险,得换 BigInt。面试里主动提溢出,是加分项。

解法二:异或分组。

异或有一个美妙的性质:相同的数两两抵消。把数组所有元素和 1 到 n+2 全部异或在一起,成对的数全消掉,最后剩下 a⊕b。a⊕b 不为零,说明 a 和 b 至少在某一位上不同。取 a⊕b 任意一个为 1 的位(惯用最低位),按「这一位是 0 还是 1」把数组元素和 1 到 n+2 全体分成两组。a 和 b 在该位不同,必然各落一组;每组内部除了一个缺失数之外全部成对。两组各自再异或一遍,各剩一个数,就是 a 和 b。

异或法比求和法多几步,但完全不担心数值范围——位运算没有溢出这回事。

写出可运行的代码。

下面这份实现同时给出两种解法,用 1..8 缺 3 和 6 的示例走查,再做两千组随机对拍(暴力集合做基准),用 Node.js 22.15.0 直接运行(node find-missing-two.js)即可:

'use strict';

// 解法一:求和 + 平方和,BigInt 防溢出;m = n+2,完整范围为 1..m
function findTwoBySum(arr, m) {
  let sum = 0n, sq = 0n;
  for (const v of arr) { sum += BigInt(v); sq += BigInt(v) * BigInt(v); }
  let fullSum = 0n, fullSq = 0n;
  for (let i = 1; i <= m; i++) { fullSum += BigInt(i); fullSq += BigInt(i) * BigInt(i); }
  const s = fullSum - sum;       // a + b
  const q = fullSq - sq;         // a² + b²
  const d = isqrt(2n * q - s * s); // |a - b| = sqrt(2(a²+b²) - (a+b)²)
  return [Number((s + d) / 2n), Number((s - d) / 2n)];
}

// 整数平方根:二分查找(BigInt)
function isqrt(x) {
  if (x < 2n) return x;
  let lo = 1n, hi = x;
  while (lo <= hi) {
    const mid = (lo + hi) / 2n;
    if (mid * mid <= x) lo = mid + 1n; else hi = mid - 1n;
  }
  return hi;
}

// 解法二:异或分组
function findTwoByXor(arr, m) {
  let x = 0;
  for (const v of arr) x ^= v;
  for (let i = 1; i <= m; i++) x ^= i; // 成对抵消后 x = a ^ b
  const bit = x & -x;                  // 取最低为 1 的位:a、b 在此位不同
  let g0 = 0, g1 = 0;
  const feed = (v) => { if (v & bit) g1 ^= v; else g0 ^= v; };
  for (const v of arr) feed(v);
  for (let i = 1; i <= m; i++) feed(i);
  return [g0, g1];
}

// 暴力基准:用 Set 找出缺失的两个数
function brute(arr, m) {
  const seen = new Set(arr);
  const missing = [];
  for (let i = 1; i <= m; i++) if (!seen.has(i)) missing.push(i);
  return missing;
}

const demo = [1, 2, 4, 5, 7, 8]; // 1..8 缺 3 和 6
console.log('示例:求和法', findTwoBySum(demo, 8), '异或法', findTwoByXor(demo, 8), '基准', brute(demo, 8));

let bad = 0;
for (let t = 0; t < 2000; t++) {
  const m = 2 + Math.floor(Math.random() * 1000);
  const a = 1 + Math.floor(Math.random() * m);
  let b = 1 + Math.floor(Math.random() * m);
  while (b === a) b = 1 + Math.floor(Math.random() * m);
  const arr = [];
  for (let i = 1; i <= m; i++) if (i !== a && i !== b) arr.push(i);
  const want = [Math.min(a, b), Math.max(a, b)].join(',');
  const ok = (r) => r.slice().sort((p, q) => p - q).join(',') === want;
  if (!ok(findTwoBySum(arr, m)) || !ok(findTwoByXor(arr, m))) bad++;
}
console.log('2000 组随机对拍,不一致组数:', bad);

实测输出:

示例:求和法 [ 6, 3 ] 异或法 [ 6, 3 ] 基准 [ 3, 6 ]
2000 组随机对拍,不一致组数: 0

两千组随机用例两种解法全部与暴力基准一致。

解法图:1 到 n+2 的数组缺了两个数 a 和 b,要求 O(n) 时间、O(1) 空间找出。左上是解法一「求和 + 平方和」:1..n+2 的理论和减去数组实际和得 a+b;理论平方和减实际平方和得 a²+b²;由 a+b 与 a²+b² 联立解出 a、b,一趟遍历两个累加器即可,注意平方和可能溢出要用更大整型。右上是解法二「异或分组」:第一步把数组所有元素与 1..n+2 全部异或,成对的数抵消,剩下 a⊕b;第二步取 a⊕b 任意一个为 1 的位(如最低位),这位为 1 说明 a、b 在该位不同,按该位把所有数分成两组,a 和 b 必然各落一组;第三步每组内部再全部异或,各自剩一个数,就是 a 和 b。底部对比:求和法直观但要防溢出;异或法多几步但完全不担心数值范围,两者都是一趟或常数趟遍历、O(1) 额外空间,思想与「找缺一个数」「找重复数」一脉相承 图 2:求和方程法与异或分组法的完整推导。

可能的追问。

「只缺一个数呢?」更简单,求和相减或全体异或,一行出答案。「缺三个呢?」数学派再加一个立方和方程,但方程组越来越脆;异或派难以直接分组,通常改用位图或分治。「数组里有重复数呢?」题设就变了,这两种方法的前提(每个数至多出现一次)被破坏,得换「找重复数」的套路。

易错点清单。

一,平方和用 32 位整型累加,大 n 直接溢出而不自知。二,异或分组时只对数组元素分组,忘了 1 到 n+2 的全量也要按同一位分组,结果两组都混进了成对的数。三,取标志位写成 x & (−x) 时没想清楚负数补码,换个语言实现就翻车。四,答完求和法不提溢出,被追问「n 是一亿呢」当场卡住。

回到面试现场,建议的答题顺序:先给求和加平方和(方程思想最直观),主动指出溢出风险,再补异或分组作为「免疫溢出」的替代。两种方法看似无关,内核是同一个:不逐个追踪元素,而是用可逆的整体聚合特征(和、平方和、异或和)做集合差异。这个思想在样本缺失检测、特征对齐里反复出现。

你在面试里被推导出「第二个方程」吗?当时想到的是平方和还是别的?欢迎分享。