C++ 位运算 高频面试考点 力扣 面试题 17.19. 消失的两个数字 题解 每日一题

文章目录


题目解析

题目链接:力扣 面试题 17.19. 消失的两个数字

题目描述:

示例 1:

输入:nums = [1]

输出:[2,3]

解释:n = 3(数组长度+2),[1,3] 中缺失的数字为 2 和 3
示例 2:

输入:nums = [2,3]

输出:[1,4]

解释:n = 4(数组长度+2),[1,4] 中缺失的数字为 1 和 4
限制:

  1. n >= 2(数组长度至少为 0,对应 n=2 时缺失 1 和 2)
  2. 数组中所有元素均唯一且在 [1, n] 范围内
  3. nums.length <= 30000

为什么这道题值得你花几分钟弄懂?

这道题是位运算经典场景的"组合升级题" ------它融合了力扣136. 丢失的数字力扣260. 只出现一次的数字III 的核心逻辑。其价值不在于"暴力枚举找缺失",而在于掌握 "用异或拆分问题、精准定位目标"的高效思维,这是面试中考察"位运算深度应用"和"问题拆解能力"的高频考点。

面试官考察这道题时,核心关注三点:

  1. 能否联想到"异或的特性",将"找两个缺失数"转化为"找两个唯一出现一次的数",体现问题转化能力;
  2. 能否通过"异或结果的最低有效位"拆分集合,将两个目标数分离到不同组,暴露对位运算细节的掌握;
  3. 能否优化"找最低有效位"的代码(如用 sum & (-sum)),彰显对位运算技巧的熟练程度。

强烈建议先去看一看这两道题:

算法原理

核心公式👇(详细推导可参考位运算 常见方法总结 算法练习):

x ^ x = 0
相同数字异或结果为 0:两个完全一致的二进制数,对应位完全相同,异或后每一位均为 0;
0 ^ x = x
0 与任何数字异或结果为该数字本身:0 的所有二进制位均为 0,与其他数字异或时,结果完全跟随另一数字的二进制位。
(tmp >> diff) & 1

公式的作用是精准判断 tmp 的二进制表示中,第 diff 位(从 0 开始计数)的数值是 0 还是 1

1. 用异或得到"两个缺失数的异或结果"

结合题目场景来看:数组 nums 包含 n-2 个不同整数,且所有整数都在 [1, n] 范围内(其中 n = nums.size() + 2)。这意味着 [1, n] 中恰好有 2 个数字未出现在 nums 中,也就是我们要找的目标数(记为 ab)所以我们就可以按照丢失的数字那道题的方法进行统计。

即我们可以将"数组元素"与"[1, n] 所有数字"视为一个完整的"对比集合",对两者进行全量异或:

  • 对于 [1, n] 中出现在 nums 里的数字 :它会在"数组元素异或"中出现 1 次,在"[1, n] 数字异或"中也出现 1 次。根据 x ^ x = 0,这两次异或会相互抵消,最终贡献为 0;
  • 对于 [1, n] 中未出现在 nums 里的两个目标数 ab :它们仅在"[1, n] 数字异或"中出现 1 次,在"数组元素异或"中完全未出现。根据 0 ^ x = x,这两个数不会被抵消,最终会保留在异或结果中。

因此,全量异或的最终结果,本质就是两个目标数的异或值,即 tmp = a ^ b

2. 找到异或结果中"最低为1的位",以此为依据拆分集合

首先要明确一个关键前提:第一步得到的异或结果 tmp = a ^ bab 为两个缺失数)必然不为 0。

因为 ab[1, n] 中两个不同的数字,它们的二进制表示至少存在一处差异------而异或运算的核心特性是"对应位不同则为 1",这就意味着 tmp 的二进制中至少有一位是 1 (这一位正是 ab 二进制差异的直接体现)。

接下来,我们需要定位到 tmp 中"最低为 1 的位",这里我们通过位运算公式 (tmp >> diff) & 1

  • 通过逐步增大 diff(从 0 开始计数),将 tmp 的二进制向右移动 diff 位,让目标位移动到最低位;
  • 再与 1 进行按位与运算,若结果为 1,说明当前 diff 对应的位就是 tmp 中"最低为 1 的位"(记为第 diff 位)。

找到这一位后,它就成了拆分数字的"天然依据":

由于 tmp = a ^ b,且第 diff 位为 1,根据异或规则,ab 在这一位的取值必然不同 ------一个是 0,另一个是 1。

基于此,我们可以将所有数字(包括数组 nums 中的元素和 [1, n] 范围内的完整数字)拆分为两组:

  • 第一组:第 diff 位为 1 的数字(包含其中一个缺失数,假设为 a)与 diff位为1的其他数*2
  • 第二组:第 diff 位为 0 的数字(包含另一个缺失数,假设为 b)与 diff位为0的其他数*2

这样的拆分能确保 ab 被精准分到不同组中,而其他数字会因二进制位固定,成对出现在同一组里------为后续通过异或分离出缺失数做好了关键铺垫。

如下图所示👇:

3. 对拆分后的两个集合分别异或,得到两个缺失数。
我们上一步已经分好组,这样的分组会产生一个关键效果:即可以将a,b分开进行处理。

对于除 ab 之外的其他数字(既存在于数组 nums 中,也属于 [1, n] 范围),它们会在两个关联集合(数组元素和 [1, n] 区间的所有数)中各出现一次,且必然被分到同一组中(因为同一个数字的二进制位是固定的),而 ab 则会被严格分离到两个不同的组中。

我们就可以将两组分开异或运算,每组中就只剩下一个"只出现一次"的数字(即 ab),其余数字都会因为出现两次而在异或运算中相互抵消------这就将"找两个缺失数"的问题,简化为"在两个独立分组中各找一个缺失数"的基础问题。

代码实现

基础实现

cpp 复制代码
class Solution {
public:
    vector<int> missingTwo(vector<int>& nums) {
        // 步骤1:计算"两个缺失数的异或结果"
        int tmp = 0;
        // 异或数组中所有元素
        for(auto x : nums) tmp ^= x;
        // 异或1~(n)(n = 数组长度+2)
        for(int i = 1; i <= nums.size() + 2; i++) tmp ^= i;

        // 步骤2:找到异或结果中"最低为1的位"(diff是该位的掩码)
        int diff = 0;
        while(1)
        {
            // 检查tmp的第diff位是否为1
            if(((tmp >> diff) & 1) == 1) break;
            else diff++;
        }

        // 步骤3:按"第diff位是否为1"拆分集合,分别异或找缺失数
        int a = 0, b = 0;
        // 先异或数组中的元素
        for(int x : nums)
            if(((x >> diff) & 1) == 1) b ^= x;  // 第diff位为1的元素归为一组
            else a ^= x;                        // 第diff位为0的元素归为另一组
        // 再异或1~n的元素
        for(int i = 1; i <= nums.size() + 2; i++) 
            if(((i >> diff) & 1) == 1) b ^= i;
            else a ^= i;
        
        return {a, b};
    }
};

简单优化(sum & (-sum) 找最低有效位)

cpp 复制代码
class Solution {
public:
    vector<int> missingTwo(vector<int>& nums) {
        // 步骤1:同上,计算两个缺失数的异或结果(变量名改为sum,不影响逻辑)
        int sum = 0;
        for(auto e : nums) sum ^= e;
        for(int i = 1; i <= nums.size() + 2; i++) sum ^= i;
        
        // 步骤2:优化!用sum & (-sum)直接获取"最低为1的位的掩码"
        int mark = sum & (-sum);
        
        // 步骤3:同上,按mark拆分集合并异或
        int a = 0, b = 0;
        for(auto e : nums)
            if(mark & e) a ^= e;  // 简化判断:mark的唯一1位与e的对应位进行与运算
            else b ^= e;
        for(int i = 1; i <= nums.size() + 2; i++)
            if(mark & i) a ^= i;
            else b ^= i;
        
        return {a, b};
    }
};

代码优化说明

两个代码的核心算法思路完全一致 ,均基于我们上面的算法思路 与 "异或运算"的特性(x^x=0x^0=x) 相同。

两者的差异仅在于第2步"找最低有效位"的实现方式,优化版通过数学技巧简化了代码、提升了效率,具体对比如下:

1. 基础版的实现逻辑(循环判断)

基础版通过while循环逐位检查tmp的每一位,直到找到"值为1的最低位":

cpp 复制代码
int diff = 0;
while(1)
{
    // 检查tmp的第diff位是否为1:将tmp右移diff位后,与1做"与运算"
    if(((tmp >> diff) & 1) == 1) break; 
    else diff++; // 若当前位为0,diff+1,检查下一位
}

缺点 :最坏情况下需要循环32次(int类型共32位),代码稍显冗余。

2. 优化版的实现逻辑

优化版直接使用sum & (-sum)这一数学公式,一步得到"最低有效位的掩码",无需循环:

cpp 复制代码
int mark = sum & (-sum);

核心原理 :基于计算机中"补码"的存储规则(负数用"原码取反+1"表示):

sum=6(二进制000...000110)为例:

(1)-sum(即-6)的补码为:111...111010(对6的原码取反后加1);

(2) sum & (-sum)000...000110 & 111...111010 = 000...000010(仅保留"最低位为1"的位,其余为0)。

最终mark=2(二进制10),恰好对应"最低有效位的掩码",与基础版的(1 << diff)(当diff=1时,1<<1=2)效果完全一致。

优势

  • 代码简化:用1行代码替代循环,可读性更高;
  • 效率提升:无需逐位判断,直接通过一次位运算得到结果,时间复杂度从O(32)降为O(1)

3. 优化后的连锁简化(分组判断)

由于优化版得到的mark是"最低有效位的掩码"(仅某一位为1,其余为0),分组判断也可以简化:

  • 基础版需要通过(x >> diff) & 1判断"第diff位是否为1";

  • 优化版直接用mark & x判断:若结果非0,说明x的"该位为1";若结果为0,说明"该位为0":

    cpp 复制代码
    // 优化版分组逻辑(更简洁)
    if(mark & e) a ^= e;  // 等价于基础版的"((e >> diff) & 1) == 1"
    else b ^= e;          // 等价于基础版的"((e >> diff) & 1) == 0"

优化点汇总

对比维度 基础版 优化版 优化核心
找最低有效位 循环diff从0到31,逐位检查 1行代码sum & (-sum) 利用补码特性,一步得掩码
时间效率 最坏O(32)(可视为O(1) 严格O(1) 减少位运算次数
分组判断逻辑 需要右移+与运算,逻辑稍复杂 直接与mark做与运算,逻辑直观 复用掩码,简化条件判断

其他解法优劣势对比

解法 时间复杂度 空间复杂度 优势 劣势
位运算解法(我们所用的方法) O(n) O(1) ✅ 空间最优:仅用几个变量,无额外空间开销 ✅ 效率稳定:遍历次数固定为2次n,无波动 ✅ 无数据范围限制:无需担心哈希表的扩容或数组越界 ⚠️ 逻辑依赖位运算基础:需理解异或特性和补码规则,对新手不友好
哈希表暴力法 O(n) O(n) ✅ 逻辑直观:易理解,编码难度低 ❌ 空间开销大:需要存储n-2个元素,不符合"常数空间"要求 ❌ 效率受哈希表影响:极端情况下哈希表查找可能退化为O(n)
数组标记法 O(n) O(n) ✅ 逻辑简单:用数组下标对应数字,标记出现状态 ❌ 空间开销大:需创建长度为n的数组 ❌ 受n范围限制:n过大时数组会占用大量内存

面试避坑指南

  1. 拆分集合时漏异或"1~n" :步骤3必须同时异或"数组元素"和"1~n的元素",只异或其一会导致结果错误(因为需要抵消重复出现的数);
  2. 找最低位时循环条件错误 :第一版代码中,diff 从0开始递增,判断条件是 ((tmp >> diff) & 1) == 1,不要写成 ((tmp & (1 << diff)) == 0)(逻辑颠倒);
  3. 忽略"补码"导致 sum & (-sum) 用错 :仅当 sum 为非负数时该技巧有效,但本题中 sum 是异或结果,必然非负(异或不产生负数),无需额外处理;
  4. 返回结果顺序错误 :题目不要求缺失数按大小排序,返回 {a,b}{b,a} 均正确,但面试中可主动说明"若需排序可加一句 if(a > b) swap(a,b)",体现细节考虑。

总结

位运算类题目侧重于利用二进制数的特性和位操作技巧(如异或、与、或、位移等)解决问题,其核心在于:

  • 挖掘问题与二进制表示的内在联系
  • 运用数学性质简化计算(如x^x=0sum&(-sum)找最低有效位等)
  • 追求极致的时间和空间效率优化
  • 思维方式偏向数学推导和技巧应用

我们的位运算到这里就告一段落了,接下来我们一起讨论 力扣1576 替换所有的问号,我们一起学习如何通过模拟的思路,按照题目要求一步步处理字符串中的每个问号,确保最终结果满足相邻字符不同的约束条件。

如果这篇内容对你有帮助,别忘了 点赞👍 + 收藏⭐ + 关注👀 哦!

有问题欢迎在评论区留言,我会认真思考并及时回复!

相关推荐
飞哥的AI笔记3 小时前
前言:写给2025秋招同学的AI技术题库
面试
Jacob00003 小时前
[Decision Tree] H(D) & IG & IGR
算法·面试
vadvascascass3 小时前
平滑加权轮询负载均衡的底层逻辑
java·算法·负载均衡
初圣魔门首席弟子3 小时前
C++ STL string(字符串)学习笔记
c++·笔记·学习
CoovallyAIHub3 小时前
Transformer作者开源进化计算新框架,样本效率暴增数十倍!
深度学习·算法·计算机视觉
AA陈超3 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P04-12 可缩放浮点数的曲线表
c++·游戏·ue5·游戏引擎·虚幻
晓宜3 小时前
Java25 新特性介绍
java·python·算法
旭意4 小时前
C++微基础备战蓝桥杯之数组篇10.1
开发语言·c++·蓝桥杯
掘金安东尼4 小时前
前端周刊434期(2025年9月29日–10月5日)
前端·javascript·面试