算法39,位运算,消失的两个数字

题目

数组 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,33,2 都算对)

一句话总结

全部异或让出现过的数自己和自己抵消,得到 a^b;取 tmp 最右侧的 1 当分岔位,把数组和完整序列都按这一位分两组,各组内部再异或一次,成对的自会抵消,各剩一个缺失数。

思维链条 :268(缺 1 个)→ 把"数组"和"完整序列"配对异或;260(缺 2 个的反向版)→ lowbit 分组。这题是两题的合成体

相关推荐
ZiLing1 小时前
2026 ROS 2 Lyrical 踩坑实录(一):编译与依赖——rosdep、现代 CMake 与 CMake 4.x
算法
小刘在重生~1 小时前
Java 异常体系完整笔记
java·笔记·python
Murphy_lx1 小时前
1124. 表现良好的最长时间段
c++·算法
抠脚小弟1 小时前
Spring Task 定时任务详解:从入门到实战
java·后端·spring
aramae1 小时前
模拟实现strstr函数(C语言)
c语言·开发语言·算法
2601_962284501 小时前
Python 和Java 哪个更适合做自动化测试?
java·自动化测试·python·接口测试·性能测试
liliangcsdn2 小时前
因子权重矩阵处理-滞回缓冲带+降频稳定化动态重选
开发语言·python·算法
随遇而安zx2 小时前
【多线程】---AQS 原理 知识点(设计思想与源码深度解析)
java·多线程·aqs
LXMXHJ2 小时前
springboot中的线程操作
java·spring boot·后端·线程