当前位置: 首页 > news >正文

C++ 位运算 高频面试考点 力扣137. 只出现一次的数字 II 题解 每日一题

文章目录

  • 题目解析
  • 为什么这道题值得你花几分钟的时间弄懂?
  • 位运算(最优解法)
    • 算法原理
    • 代码实现
      • 时间复杂度和空间复杂度:为什么它是“最优解”?
  • 哈希表(直观解法)
    • 算法原理
    • 代码实现
      • 时间复杂度和空间复杂度的分析
  • 总结
  • 下题预告

在这里插入图片描述
在这里插入图片描述

题目解析

题目链接:力扣137. 只出现一次的数字 II

题目描述:
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。

示例 1:
输入:nums = [2,2,3,2]
输出:3
解析:数组中 2 出现 3 次,3 出现 1 次,符合“除一个元素外其余均出现三次”的条件,因此返回 3。

示例 2:
输入:nums = [0,1,0,1,0,1,99]
输出:99
解析:0 和 1 分别出现 3 次,99 出现 1 次,满足题目约束,故返回 99。

提示:
1.1 <= nums.length <= 3 * 10⁴:数组长度在合理范围内,需保证算法在大规模数据下仍高效运行;
2.-2³¹ <= nums[i] <= 2³¹ - 1:覆盖整数的全部范围,需注意处理负数的二进制表示(尤其是补码);
3.nums 中,除某个元素仅出现一次外,其余每个元素都恰出现三次。

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

如果说位运算题型里有“必学经典”,那这道题绝对能排进前三。它就像一把钥匙,不仅能帮你打开“从二进制视角拆解问题”的新大门,更能让你亲身体会算法设计中“时间与空间如何权衡”的底层逻辑,无论对思维提升还是面试应试,都大有裨益。

我们先说说它的核心价值在哪:

  • 考点踩得准,面试直接用:题目明确定死了“线性时间(O(n))”和“常数空间(O(1))”的双重要求,而这正是大厂面试里“算法优化能力”的高频考点。很多时候,面试官看的不只是“能不能做出来”,更是“能不能用最优的方式做出来”——而这道题刚好能帮你练会“用二进制操作抠空间、提效率”的关键能力。
  • 方法对比超直观,理解更透彻:解题时你会遇到两种典型思路:一种是哈希表,直观好懂但要耗额外空间;另一种是位运算,代码稍抽象却能做到空间最优。把这两种方法放一起对比,你能清晰看到“时间-空间权衡”不是一句空话——不同场景下该选哪种方案,看完这道题会有更具体的体感。
  • 举一反三能力直接拉满:它是“只出现一次的数字”系列的第二题(第一题是“其余元素出现两次”),但学会它的位运算逻辑后,你会发现这套思路能直接迁移到“其余元素出现k次”的通用场景。比如下次遇到“找只出现一次、其余出现四次”的题,你不用重新想解法,改个小细节就能用,这种“一通百通”的感觉,才是刷题的核心意义。

再从应试角度聊聊,面试官考察这道题时,其实在悄悄观察你三个层面的能力:

  1. 基础层:会不会用常规思路解题? 比如能不能想到用哈希表统计次数——这能看出你对“哈希表映射特性”这类基础数据结构的掌握程度,是入门门槛;
  2. 进阶层:能不能突破空间限制? 也就是能不能想到位运算解法——这能暴露你对“二进制位统计规律”的理解深度,是区分普通选手和优秀选手的关键;
  3. 迁移层:能不能把解法通用化? 比如问你“如果其余元素出现五次,该怎么改?”——这能看出你的算法思维是否灵活,会不会把一道题的解法,变成一类题的解题模板。

不过,要是有朋友对位运算的基础操作(比如异或、与、左移)有点忘了也不用慌。先花两分钟回顾下核心规则:异或( ^ )能模拟“无进位相加”(比如1 ^ 1=0,0 ^ 1=1,刚好对应不考虑进位的加法),而与(&)加左移(<<1)能模拟“进位计算”(比如1&1=1,左移一位后就是进位值)。这两个是解决本题的“核心工具”,要是想更系统地复习,也可以看看这篇博客:位运算 常见方法总结 算法练习 C++,帮你快速捡回知识点~

位运算(最优解法)

算法原理

其实这道题的核心思路特别朴素——既然数组里“除了一个数出现1次,其他都出现3次”,那我们不妨把目光聚焦到二进制的每一位上。毕竟不管数字多大,最终都得拆成0和1的组合,而“出现3次”这个规律,在二进制位上会体现得格外明显。

你可以这么理解:把数组里所有数字都转成二进制,然后像“列竖式”一样纵向看每一位(比如从最低位的第0位,到最高位的第31位)。对于那些出现3次的数字,它们在某一位上如果是1,那这个1肯定会“凑够3个”;而那个只出现1次的目标数字,要是它在某一位上是1,就会让这一位的1的总数多出来1个。

基于这个特点,我们只需要做两步,就能推出目标数字每一位的值:

  1. 统计每一位的1的总数:比如看第0位(最右边那一位),把所有数字在这一位上的1都数一遍;再看第1位,重复同样的操作,直到把32位都统计完。
  2. 用“取余3”判断目标位的值:因为除了目标数字,其他数字的1都会凑成3的倍数,所以对每一位的1的总数除以3取余数,结果只有两种可能:
    • 余数是0:说明这一位的1刚好是3的倍数,目标数字在这一位肯定是0(没多出来的1);
    • 余数是1:说明这一位的1比3的倍数多1个,这个多出来的1只能来自目标数字,所以目标数字在这一位是1。

结合这张二进制位统计图,你能更直观地看到这个规律:
在这里插入图片描述
图里每一位的统计结果,要么是“3的倍数(3n)”,要么是“3的倍数加1(3n+1)”,取余3之后的结果,和目标数字对应位的0/1完全一样——这就是位运算解法的“核心密码”。

可以自己拿起笔和纸结合下面这个例子自己来验证一下:
比如数组[2,2,3,2]

  • 2的二进制是10,3的二进制是11
  • 看第0位(最低位):3个2的第0位都是0,1个3的第0位是1 → 1的总数是1 → 取余3得1 → 目标数字第0位是1;
  • 看第1位:3个2的第1位都是1(3个1),1个3的第1位是1 → 1的总数是4 → 取余3得1 → 目标数字第1位是1;
  • 更高的位上所有数字都是0 → 取余3得0 → 目标数字这些位都是0;
  • 最后把这些位组合起来,就是11(也就是3),正好是我们要找的答案。

代码实现

明白了原理,代码就很好理解了。我们要做的就是“遍历每一位→统计1的个数→取余判断→拼出结果”,一步一步把刚才的思路落地:

注:代码中出现的两个位运算公式的详细推导可以看 位运算 常见方法总结 算法练习 C++ )

class Solution {
public:int singleNumber(vector<int>& nums) {int ret = 0; // 用来存最终结果,一开始是0(二进制全0)// 遍历int的32位:从最低位(第0位)到最高位(第31位),一个都不能漏for (int i = 0; i < 32; i++) {int sum = 0; // 临时变量,统计当前第i位上所有数字中1的总数// 逐个检查数组里的每个数字,看它们的第i位是不是1for (auto e : nums) {// if括号中的位运算公式是判断e的二进制表示中的第i位是否为1,即注释中说明的第一个公式if (((e >> i) & 1) == 1) {sum++; // 是1的话,就把统计数加1}}// 取余3,判断目标数字第i位是0还是1sum %= 3;// 如果余数是1,说明目标数字第i位是1,得把这个1“拼”到结果里if (sum == 1) {ret |= (1 << i);// 这里的位运算公式是将ret的二进制表示的第i位修改成1,即注释中说明的第二个公式}}return ret; // 把32位拼好的结果返回,就是那个只出现一次的数字}
};

关键补充:负数补码的兼容处理
题目中nums包含负数(范围到-2³¹),但上述代码无需额外修改即可兼容,核心原因是:
C++中int类型以补码存储,负数的符号位(第31位)为1,正数为0。统计第31位时,“sum%3”的逻辑与其他位完全一致——若sum%3=1,说明目标数字是负数(符号位为1),通过ret |= (1 << 31)会自然将ret转为补码形式,最终返回的就是正确的负数值。

时间复杂度和空间复杂度:为什么它是“最优解”?

  • 时间复杂度:O(n)
    这里有两层循环,但外层循环固定只跑32次(因为int类型就32位,不管数组多长都不会变,属于“常数级操作”);内层循环要遍历整个数组,跑n次。所以总操作次数其实是“32×n”——严肃的来讲时间复杂度应该是O(nlogC),其中 n 是数组的长度,C 是元素的数据范围,在本题中 logC = log 2³² = 32),根据大O记号的规则常数(32)可以忽略,最终时间复杂度就是O(n),完全符合题目“线性时间”的要求。

  • 空间复杂度:O(1)
    整个过程只用到了3个变量:ret存结果、sum统计当前位的1、i控制遍历的位数。不管数组长度是10还是10万,这些变量的数量都不会变,没有额外占用和数组长度相关的空间,所以是“常数级空间”,完美满足题目最严格的约束。

哈希表(直观解法)

算法原理

核心思路:利用哈希表的“键-值”映射特性,统计每个元素的出现次数,最后筛选出次数为1的元素

具体步骤:
1.初始化哈希表(C++中可用unordered_map<int, int>),键为数组元素,值为该元素的出现次数;
2.遍历数组,对每个元素:若已在哈希表中,次数加1;若未在哈希表中,添加到哈希表并设次数为1;
3.遍历哈希表,找到值为1的键,该键即为目标元素。

该方法的优势是逻辑直观,无需深入理解二进制操作,适合刚接触这类题目时快速建立解题思路;但缺点是空间复杂度较高,不满足题目“常数级空间”的最优要求,可作为“过渡解法”理解。

代码实现

#include <unordered_map>class Solution {
public:int singleNumber(std::vector<int>& nums) {std::unordered_map<int, int> countMap; // 键:数组元素,值:出现次数// 第一步:统计每个元素的出现次数for(int num : nums){countMap[num]++; // 若num已存在,次数+1;否则添加到哈希表并设为1}// 第二步:遍历哈希表,找到次数为1的元素for(auto& pair : countMap){if(pair.second == 1)return pair.first; // 找到目标元素,直接返回}return 0; // 理论上不会执行(题目保证有唯一解)}
};

时间复杂度和空间复杂度的分析

  • 时间复杂度:O(n)(平均)
    遍历数组统计次数(O(n)),遍历哈希表筛选结果(哈希表元素个数最多为(n-1)/3 +1,即O(n))。std::unordered_map的插入与查询平均时间复杂度为O(1),但最坏情况下(哈希冲突严重)会退化为O(n),此时总时间复杂度接近O(n²)——不过LeetCode题目用例中哈希冲突概率极低,实际可认为满足“线性时间”要求。

  • 空间复杂度:O(n)
    哈希表存储的元素个数取决于数组中不同元素的数量,最坏情况下(如目标元素外的其他元素均不重复,但题目中其他元素均出现三次,实际不同元素个数为(n-1)/3 +1),空间复杂度仍为O(n),不满足“常数级空间”约束。

总结

  1. 方法对比与选择
对比维度位运算解法哈希表解法
时间复杂度O(n)(32×n,常数可忽略)O(n)(平均),O(n²)(最坏)
空间复杂度O(1)(常数级,最优)O(n)(依赖不同元素数量)
适用场景需满足“常数空间”约束追求逻辑直观,无空间限制
理解门槛较高(需位运算知识铺垫)较低(依赖哈希表基础)
  1. 核心知识点回顾
    位运算:通过右移(>>)、与(&)、或(|)、左移(<<)实现二进制位的操作与统计;
    哈希表:利用“键-值”映射快速统计元素出现次数,平均时间复杂度为O(1)的插入与查询。

  2. 解题思维迁移
    本题的位运算思路可直接推广到“其余元素出现k次,找只出现1次的元素”的场景,核心修改仅一处:将“sum%3”改为“sum%k”。

示例:若题目要求“其余元素出现4次,找只出现1次的元素”,只需将代码中sum %= 3改为sum %= 4。以数组[5,5,5,5,7]为例:

  • 5的二进制为101,4次出现后,每一位的1总数均为4的倍数(sum%4=0);
  • 7的二进制为111,每一位的1会让sum多1,sum%4=1,最终拼接结果为111(即7),与预期一致。

下题预告

接下来,我们将聚焦「数组中缺失元素」的经典延伸问题——力扣 面试题 17.19. 消失的两个数字。

这道题的核心场景极具代表性:给定一个包含 n-2 个不同整数的数组,所有元素均在 [1, n] 范围内(n ≥ 2),要求找出 [1, n] 中缺失的那两个数字,且需尽可能优化时间与空间复杂度。

前置思考:不妨先试着想两个问题,为下一题的学习铺垫思路:

  1. 若先计算 [1, n] 的总和与数组总和的差值,能得到“两个缺失数字的和”,但如何进一步把这两个数分开?
  2. 借鉴“异或分组”的思路:若对 [1, n] 所有数和数组所有数做异或,最终结果是什么?如何利用这个结果将两个缺失数字分到不同组中?

带着这些疑问,我们下一题将逐步拆解最优解法,我们一起掌握“找两个缺失数字”的核心逻辑!

http://www.dtcms.com/a/453023.html

相关文章:

  • 商城网站开发与设计郑州seo外包阿亮
  • 自建本地DNS过滤系统:实现局域网广告和垃圾网站屏蔽
  • 《投资-90》价值投资者的认知升级与交易规则重构 - 第三层:DCF算的未来多少年的现金总和?
  • 网站开发的套路龙腾盛世网站建设
  • .NET周刊【9月第3期 2025-09-21】
  • 建站模板哪里好关于音乐的个人网站
  • 力扣第470场周赛
  • leetcode滑动窗口(C++)
  • 企业网站建设代理公司intitle 网站建设
  • 多模卫星导航定位与应用-原理与实践(RTKLib)6
  • PSP用PS1(PSX)中文游戏合集
  • 吴恩达机器学习课程(PyTorch适配)学习笔记:1.3 特征工程与模型优化
  • golang面经——GC模块
  • 微信小程序中的双线程模型及数据传输优化
  • 网站建设最流行语言电商网站设计岗位主要是
  • 《投资-77》价格投机者如何重构认知与交易准则 - 现成的常见工具
  • 专业的手机网站建设公司排名搜狐快站怎么做网站
  • 测试Meta开源的 OpenZL 无损压缩框架
  • vue3 两份json数据对比不同的页面给于颜色标识
  • XSLFO 流:从XML到PDF的转换之道
  • 2025-10-7学习笔记
  • 基于websocket的多用户网页五子棋(七)
  • 做网站pyton电子商务网站建设收获
  • 合肥佰瑞网站竞价网站做招商加盟可以不备案吗
  • Java “并发容器框架(Fork/Join)”面试清单(含超通俗生活案例与深度理解)
  • 网站建设基础实训报告网站做关键词排名每天要做什么
  • 阿里云服务器安装MySQL服务器
  • 苏州展示型网站建设uc网站模板
  • 智能体框架大PK!谷歌ADK VS 微软Semantic Kernel
  • Ubuntu 24.04 SSH 多端口监听与 ssh.socket 配置详解