news 2026/9/7 21:25:23

LeetCode 136题:异或运算巧解“只出现一次的数字”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 136题:异或运算巧解“只出现一次的数字”

1. 读题,先搞清楚这是道什么题

1.1 题目真正在考察什么

LeetCode上面的136题“只出现一次的数字”,题目本身短得有点不像话:给你一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次,找出那个只出现了一次的元素。

看起来就是道简单题,但我刷了这么多年题,发现这题特别有意思。它虽然是“简单”难度,却在面试里经常出现,而且考的往往不是能不能做出来,而是你用什么方式做出来。能AC的人一抓一大把,但能用最优雅方式做出来的人,可能不到十分之一。

先说几个前提条件。第一,数组非空,也就是说不用考虑空的输入。第二,只有一个元素是落单的,其他所有人都是一对一对出现的。第三,也是最重要的,题目要求你实现线性时间复杂度的解法,并且尽量不使用额外空间。

很多人第一眼看到这题,脑子里蹦出来的方案通常是:暴力遍历、哈希表、排序。这三种方案都能做,但如果你仔细抠题目,会发现它真正的潜台词是:能不能用一种线性的、不使用额外空间的算法把它解决了。这才是这题的核心考点。换句话说,题目表面上在考数组和查找,实际上在考验你对位运算的掌握程度。

我自己的习惯是,拿到一道题先不急着写代码,先想想“如果我是出题人,我想考什么”。136题出题人真正想让你意识到的东西,就是异或运算的那几个性质。这些东西大学课本里都写过,但真正遇到题目的时候,能第一时间反应过来的,真的不多。

1.2 三种常规思路,为什么总差那么一点

先说最直观的思路。

第一种,暴力双重循环。遍历每个数,再看它有没有其他相同的数,复杂度O(n²),测试用例一大就超时。除非你只想练习语法,否则基本不用考虑。

第二种,用哈希表/集合。把出现过的数存进集合,重复出现的就删掉,最后剩下的那个就是答案。或者用哈希表记录出现次数,最后遍历找出次数为1的。这个方案时间复杂度O(n),空间复杂度也是O(n),能AC,但不符合“不使用额外空间”的进阶要求。面试官很多时候会顺着这个思路继续追问:能不能不用额外空间?

第三种,先排序再遍历。排序后相同的数会相邻,遍历一遍就能找到落单的数。时间复杂度取决于排序算法,一般是O(n log n),排序本身可能还要额外空间(比如归并排序)。这条路同样不符合要求。

我之所以把这三种都列出来,不是让你们背下来当反面教材,而是想说明一个道理:在做题的时候,你会走的弯路恰恰是理解最优解最好的垫脚石。正是因为哈希表需要O(n)空间、排序需要O(n log n)时间,你才会意识到,要同时满足“线性时间”和“常量空间”,大概率需要某种暴力解法之外的技巧,而这个技巧就是位运算。

1.3 位运算思路是怎么想到的

我第一次做这题的时候,其实也没第一时间想到异或。我是先写了哈希表的解法,然后提交通过了,再看讨论区才发现有位运算的妙解。那一刻还挺震撼的,因为代码就一行循环加一个异或赋值,简洁得不像话。

后来我养成一个习惯:看到“出现两次”“成对出现”“找唯一”这种字眼,脑子里就会自动拉响警报,优先考虑异或。这跟肌肉记忆差不多,见多了之后就会形成条件反射。

思路是这样的:两个相同的数异或的结果是0,任何一个数和0异或的结果还是它自己。所以你只要把数组里所有元素挨个异或一遍,所有成对出现的数字都会互相抵消变成0,最后剩下的数字就是数组里那个唯一的、只出现了一次的数字。整个过程只需要遍历一次数组,时间O(n),只需要一个变量存结果,空间O(1)。干净利落,满足所有要求。

2. 核心细节:异或运算如何一剑封喉

2.1 异或的四个性质

要理解这个解法,异或运算的四个基本性质必须滚瓜烂熟:

  • 归零律:a ^ a = 0。任何数和自己异或,结果是0。
  • 恒等律:a ^ 0 = a。任何数和0异或,结果还是它自己。
  • 交换律:a ^ b = b ^ a。异或运算和顺序无关。
  • 结合律:a ^ b ^ c = a ^ (b ^ c)。异或运算可以任意加括号。

拿[2, 3, 2, 4, 3]举例。把所有数异或起来:2 ^ 3 ^ 2 ^ 4 ^ 3,因为交换律和结合律,可以重排成(2 ^ 2) ^ (3 ^ 3) ^ 4,两两抵消,最后等于0 ^ 4,也就是4。就这么简单。

我之前给朋友讲这个解法的时候,他说怎么感觉跟“消消乐”似的。我说这个类比很贴切,两个一样的数字一碰就消失,最后剩下的那个就是答案。背后的数学原理其实就是二进制的逐位运算,但理解到“成对抵消”这个层面,已经足够应付大多数场景。

2.2 用生活场景理解异或去重

从一个更直观的角度来看,异或运算可以理解成“不带进位的二进制加法”。1 ^ 1 = 0,0 ^ 1 = 1,0 ^ 0 = 0,你会发现它和二进制加法的区别只是不产生进位。

这个性质在题目里可以这样理解:数组里的每个数字都是成对出现的,每一对在异或过程中都会抵消,就像两个作用力相等方向相反的力相互抵消一样。因为所有成对的数据都是成对消失的,所以落单的那个数字从头到尾都不会被抵消,它就是最终结果。

关键点在于,异或运算不关心这些数字出现的顺序,也不关心数字是正是负,它只看二进制位上的值。这就是为什么它能用一个变量搞定,不需要额外的数据结构。你不需要记每一个数字出现的次数,只需要一个“累加器”把整个数组过一遍。

2.3 时空复杂度完美满足要求

这个解法的复杂度分析非常简单。遍历长度为n的数组,每个元素参与一次异或运算,时间复杂度就是O(n)。整个过程中只需要一个整数变量来保存累加结果,不随输入规模变化,空间复杂度是O(1)。

这两条恰好精准命中题目的两个要求:“线性时间复杂度”和“不使用额外空间”。所以这个解法在LeetCode的题目设定下,可以算是最优解了。

我经常看评论区,发现很多人会用哈希表AC之后就不管了。这种学习方法其实有点可惜,因为这题的位运算解法才是真正的精华。你花同样的时间,如果只掌握了哈希表做法,那你就错过了位运算这个面试中的高频考点,后面刷到其他位运算题目可能还要重新摸索。

3. 实操过程:代码实现与踩坑记录

3.1 Python版本,最简洁的写法

直接上代码:

def singleNumber(nums): res = 0 for num in nums: res ^= num return res

就这么几行。res从0开始,遍历所有数字,逐个异或,最后返回res。我第一次看到这个解法的时候,第一反应是:就这?对,就这。但越是简单的东西,背后的思考越不简单。

如果要更“Pythonic”一点,还可以用reduce一行写完,但那样可读性反而变差了。我个人建议刷题的时候优先写清晰可读的版本,面试的时候也一样,能讲清楚思路比秀语法技巧重要得多。

3.2 C++和Java实现,注意类型范围

C++版本几乎和Python一样:

class Solution { public: int singleNumber(vector<int>& nums) { int res = 0; for (int num : nums) { res ^= num; } return res; } };

Java版本也是同样的套路:

class Solution { public int singleNumber(int[] nums) { int res = 0; for (int num : nums) { res ^= num; } return res; } }

这三种语言写出来的东西本质上是一样的。唯一的细微差别是:C++和Java的int是32位有符号整数,Python的int是任意精度整数。在这道题的范围里,int完全够用,但如果数字特别大,Python不会溢出,C++和Java要小心边界值。好在这道题的输入范围是-2^312^31 - 1,恰好是32位int的范围,所以不会出问题。

这里有个小细节值得注意:C++里用vector<int>传参,如果你传的是引用,可以避免一次拷贝,刷题这么写没问题。但如果是实际工程代码,位运算的意图最好加个注释,不然同事看了可能会一头雾水。

3.3 JavaScript和Go的版本差异

JavaScript也能用一样的思路,但有一个隐藏的坑:JS里的位运算是先把数字转成32位有符号整数来算,再转回浮点数。对于题目给出的输入范围,这个转换不会有问题,但如果你自己加大数据量测试,可能会遇到意想不到的结果。我建议用JS刷这题的时候,就用官方给出的输入范围来测,别自己去挑战超范围的值。

var singleNumber = function(nums) { let res = 0; for (let num of nums) { res ^= num; } return res; };

Go版本也很直接:

func singleNumber(nums []int) int { res := 0 for _, num := range nums { res ^= num } return res }

总体来说,这题的代码实现在所有主流语言里几乎不存在理解门槛。真正的门槛在于“你想不想得到用异或”。

4. 常见问题与排查技巧实录

4.1 用哈希表AC了,但没达到进阶要求,要紧吗

很多同学问过我:我用哈希表做出来了,时间复杂度也是O(n),面试官会不会不满意?

我的看法是:能AC说明你基础的解题能力没问题,但如果你没体现出“优化”的意识,面试官有可能会觉得你停留在“能跑就行”的层次。毕竟这道题明确写了进阶要求,你看到了但没往那个方向想,这本身就是一个信号。

正确的回答思路是:先讲清楚哈希表方案,然后说“这个方案是O(n)时间和O(n)空间,但题目要求尽量不用额外空间,所以我再想一个更优的方案”,然后引出异或解法。这样既展现了你掌握基础解法,又体现了你追求更优方案的能力。

4.2 异或能处理负数和0吗

能,而且处理得很好。二进制的异或运算是按位操作的,和数字的正负没有关系。负数在计算机里以补码形式存储,异或运算的时候直接按补码的每一位来算,结果依然正确。0也是同理,任何数和0异或都是它自己,所以0不会影响结果。

有同学担心“两个负数异或会不会因为符号位产生奇怪的结果”,完全不会。你可以随便拿几个数手算验证一下,或者自己跑一段代码用负数测试,比如[-1, -1, -2],答案就是-2。

4.3 边界情况有哪些容易踩的坑

第一个就是数组长度。题目说数组非空,但如果你在写通用工具函数,最好还是加个空数组返回0或者抛异常的逻辑。不然传个空数组进去,你的函数会返回0,这个结果在数学上没有意义,在工程代码里可能会导致bug。

第二个是数组长度为1的情况。只有一个数的时候,不用任何操作,那个数就是答案。异或解法天然处理了这种情况,因为res从0开始,0异或任何数都等于它自身。

第三个是重复数字不相邻的情况。比如[1, 2, 1]这种,如果你用排序的思路,排序后是[1, 1, 2],也能找到2。但如果你没排序直接在循环里做相邻判断,就会出错。异或解法不受顺序影响,所以完全没有这个问题。

4.4 同类题型的排查经验

做完136题之后,你可能会碰到一个很迷惑的现象:明明思路一样的题,换个条件就不会做了。其实是因为异或解法只适用于“其他数字出现偶数次”的场景。

  • 所有数出现两次,只有一个数出现一次:用异或。
  • 所有数出现三次,只有一个数出现一次:异或就没那么好使了,需要位运算配合状态统计。
  • 有两个数只出现一次,其他都出现两次:需要分组异或。

遇到这些变体,如果你还是死板地套136题的代码,肯定做不出来。我一开始刷137题的时候也栽过跟头,后来才意识到,题目变了,底层思路也得跟着变。136题是异或的“标准题”,但异或只是整个位运算家族的冰山一角,刷完136题之后,后续的变体题同样值得花时间琢磨。

5. 从这题延伸出来的位运算思考与刷题路线

5.1 位运算在LeetCode中的典型应用场景

136题刷完之后,我建议你顺手做几道跟位运算相关的题,形成体系。我自己刷下来,感觉最值得做的是这几种类型:

  • 判断奇偶性:n & 1,比n % 2更快也更常见。
  • 交换两个数:a ^= b; b ^= a; a ^= b。这个技巧面试偶尔会聊到。
  • 移除最后一个1:n & (n - 1)。在很多位运算题里都有奇效。
  • 判断是否为2的幂:n > 0 && (n & (n - 1)) == 0。
  • 找出数组中只出现一次的数字:也就是136题。

把这些基础位运算技巧都过一遍,你会发现很多“看起来需要用复杂数据结构”的题,最后都能用几行位运算解决。这样你刷题的时候就会多一把钥匙。

5.2 136题的后续变体题

我刚才提到过几个变体,这里重点说说。

137题“只出现一次的数字 II”:每个元素出现三次,只有一个出现一次。这个题的解法就不能直接用异或了,需要统计每一位上1出现的次数,然后对3取模。本质上是用一个有限状态机去模拟“三进制”的进位,有点绕,但理解之后会对位运算认识更深。

260题“只出现一次的数字 III”:有两个元素各出现一次,其他都出现两次。思路是先全员异或得到两个答案的异或值,然后用这个异或值里的某个非零位把原数组分成两组,分别异或,就能得到两个数。第一次做的时候可能觉得“这也太巧妙了”,但做多了就会明白,这本质上是在利用异或结果中的信息做分组。

这几道题如果全刷完,你对位运算的理解会比大多数人深一大截。

5.3 实际操作中的几个小建议

刷题和做工程有一个很大的区别:刷题的代码往往很短,但优化的空间很大。我自己回刷136题的时候,除了用最简单的异或解法,还会刻意想想有没有其他写法、别的语言会不会有差异、能不能和HashMap解法做个对比测试。

还有一个建议是“手算验证”。别光看题解,拿着笔在纸上写几个小例子,比如[2, 3, 2]或者[4, 1, 2, 1, 2],手动算一遍异或过程。这个过程会帮你把“异或抵消”这个概念从“好像懂了”变成“真懂了”。

最后就是复杂度分析的表达能力。面试的时候不只是让你写代码,还要你讲清楚思路。能把“为什么异或能找出唯一数字”讲得清清楚楚,比闷头把代码写出来更重要。这也是我每次复盘这题都会反复练的东西。

做这道题,我最大的体会是:它看起来简单,但天花板其实很高。你可以用最简单的方式AC,也可以顺着它一路刷到位运算的进阶题型。如果你刚开始刷LeetCode,我建议从这题入手,既能建立信心,又能学到真东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 21:22:48

半导体术语学习指南:从PDF到产线实战,掌握OEE、SEMU与失效机理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 21:20:10

排队论驱动的服务系统优化:从M/M/c模型到实际落地

1. 从“排队”到“系统”&#xff1a;这个项目到底在研究什么 1.1 为什么排队是系统设计的核心线索 有朋友在银行运营部门工作&#xff0c;年底被领导问了一个问题&#xff1a;网点到底配几个柜员合适&#xff1f;配多了人力成本超标&#xff0c;配少了客户投诉不断。他一开始…

作者头像 李华
网站建设 2026/9/7 21:19:29

2026年厦门专业Python人工智能培训品牌深度解析与诚信

到了2026年, 人工智能技术持续对各行各业进行重塑, 在这样的时期, 把握住以某所掌握的人工智能开发能力为中心的要点, 已然变成了个人职业能够实现突破以及企业达成数字化转型的关键所在。厦门市场对于培训的需求在不断增长, 在这种情形下, 怎样去辨别一家既拥有专业方面的深度…

作者头像 李华
网站建设 2026/9/7 21:18:30

Navicat for MySQL下载安装与连接配置完整指南

一、为什么我最终还是回归了 Navicat 先说一个可能让不少新手困惑的点&#xff1a;市面上 MySQL 图形化管理工具一大堆&#xff0c;免费的、开源的、网页版的要多少有多少&#xff0c;为什么还有这么多人搜索"Navicat for MySQL"&#xff1f;因为搜这个词的人&#x…

作者头像 李华
网站建设 2026/9/7 21:17:17

vscode+Chrome MCP:让AI接管浏览器操作的全链路指南

很多开发者第一次听到“vscodechrome mcp实现AI完成页面操作”的时候&#xff0c;第一反应多半是&#xff1a;这不就是浏览器自动化吗&#xff1f;Playwright、Selenium不都能干这事&#xff1f;但如果你真正上手试过&#xff0c;会发现完全不是一回事。MCP&#xff08;Model C…

作者头像 李华
网站建设 2026/9/7 21:14:39

华为MetaERP Oracle EBS 与 Oracle Fusion 在应付(AP)模块的设计上,既体现了企业级财务软件一脉相承的核心逻辑,又因技术架构的代际差异展现出了截然不同的实现方式。以下为

Oracle EBS 与 Oracle Fusion 在应付&#xff08;AP&#xff09;模块的设计上&#xff0c;既体现了企业级财务软件一脉相承的核心逻辑&#xff0c;又因技术架构的代际差异展现出了截然不同的实现方式。以下为您详细拆解两者在设计哲学、底层逻辑、数据模型及后台程序上的深度对…

作者头像 李华