题目描述
思路解析
一句话答案:LeetCode 136 只出现一次的数字最优解是异或一趟扫描:利用「相同的数异或抵消成 0、任何数异或 0 还是自己」,把数组所有数依次异或进一个累加器,成对出现的数全部抵消,最后剩下的就是那个落单的数。时间 O(n)、空间 O(1),恰好满足题目「线性时间、常量空间」的硬要求。
这道题的隐藏约束是常量空间
数组里除了一个数只出现一次,其余每个数都恰好出现两次,要把落单的那个找出来。题目额外钉了两条硬要求:线性时间、常量空间。找出现次数本身不难,难的是这两条约束把最顺手的工具全排除了——排序要 O(n log n) 时间,哈希表计数要 O(n) 额外空间。这道题真正考的,是知不知道有一种运算能在不占空间的前提下「记住并抵消」成对的数。
为什么只出现一次的数字要用异或
先看直觉解卡在哪:用哈希表统计每个数出现几次,再挑出计数为 1 的,时间 O(n) 达标,但表本身要 O(n) 空间,不满足常量空间。我们需要的是一种「聚合」——扫过所有数后只留一个变量,且成对的数在聚合过程中自动消失。
异或(按位比较,两位不同得 1、相同得 0)正好有这两条性质:a ⊕ a = 0,相同的数碰面就归零;a ⊕ 0 = a,零是不改变结果的单位元。于是把全部数异或进一个累加器 ans,出现两次的数两两抵消成 0,一串 0 异或下来不留痕迹,最后 ans 里剩的就是那个没有搭档的数。整段代码只有一个循环、一行 ans ^= x。
成对的数隔得很远,为什么也能抵消
异或满足交换律和结合律,一长串异或的结果与运算顺序无关——可以在脑子里把相同的数重排到相邻位置再算,结果不变。所以两个 5 无论一个在开头一个在结尾,最终都等价于紧挨着异或、抵消成 0。拿题目示例 [4,1,2,1,2,5,5,3,3,6,6] 验证:1、2、5、3、6 各自成对消光,只剩 4,与答案一致。这也解释了为什么累加器从 0 起步最干净:0 是单位元,第一个数异或进来天然等于它自己,不需要任何特殊处理。
复杂度怎么算,这招的适用边界在哪
时间 O(n):数组扫一遍,每个数做一次异或。空间 O(1):只用一个变量 ans,与数组规模无关——这正是哈希表方案给不了的。
但要清楚这招的边界:异或抵消靠的是「恰好成对」。如果题目改成其余数都出现三次、只有一个出现一次(LeetCode 137),三个相同的数异或剩下它自己,抵消失效;那类题得按二进制位统计每一位上 1 出现的总次数,对 3 取模来还原答案。面试里被追问变体时,能点出「异或只消偶数次出现」这条本质,比背出代码更值钱。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住两句话:相同的数异或抵消成 0,任何数异或 0 还是它自己。把全部数异或一遍,成对的都消掉,剩下的就是落单那个。
先准备一个累加器 ans = 0。指针还没出发,接下来从下标 0 开始,把每个数依次异或进 ans。
指针走到下标 0,这一格的值是 4。把它异或进累加器:当前 ans = 0,下一步算 0 ⊕ 4。
算出 0 ⊕ 4 = 4,ans 更新为 4。
指针走到下标 1,这一格的值是 1。把它异或进累加器:当前 ans = 4,下一步算 4 ⊕ 1。
算出 4 ⊕ 1 = 5,ans 更新为 5。
指针走到下标 2,这一格的值是 2。把它异或进累加器:当前 ans = 5,下一步算 5 ⊕ 2。
算出 5 ⊕ 2 = 7,ans 更新为 7。
指针走到下标 3,这一格的值是 1。把它异或进累加器:当前 ans = 7,下一步算 7 ⊕ 1。
算出 7 ⊕ 1 = 6,ans 更新为 6。
指针走到下标 4,这一格的值是 2。把它异或进累加器:当前 ans = 6,下一步算 6 ⊕ 2。
算出 6 ⊕ 2 = 4,ans 更新为 4。
指针走到下标 5,这一格的值是 5。把它异或进累加器:当前 ans = 4,下一步算 4 ⊕ 5。
算出 4 ⊕ 5 = 1,ans 更新为 1。
指针走到下标 6,这一格的值是 5。把它异或进累加器:当前 ans = 1,下一步算 1 ⊕ 5。
算出 1 ⊕ 5 = 4,ans 更新为 4。
指针走到下标 7,这一格的值是 3。把它异或进累加器:当前 ans = 4,下一步算 4 ⊕ 3。
算出 4 ⊕ 3 = 7,ans 更新为 7。
指针走到下标 8,这一格的值是 3。把它异或进累加器:当前 ans = 7,下一步算 7 ⊕ 3。
算出 7 ⊕ 3 = 4,ans 更新为 4。
指针走到下标 9,这一格的值是 6。把它异或进累加器:当前 ans = 4,下一步算 4 ⊕ 6。
算出 4 ⊕ 6 = 2,ans 更新为 2。
指针走到下标 10,这一格的值是 6。把它异或进累加器:当前 ans = 2,下一步算 2 ⊕ 6。
算出 2 ⊕ 6 = 4,ans 更新为 4。
扫到末尾,所有成对的数都两两抵消了,ans = 4。它正是数组里那个只出现一次的数,也就是最终答案。
三个高频追问:为什么是常量空间、出现三次的变体怎么办、以及异或的运算律。
参考代码
def singleNumber(nums): ans = 0 # 累加器,从 0 开始 for x in nums: ans ^= x # 把每个数异或进来 return ans # 成对的都抵消,剩下落单的复杂度
- 时间:O(n),从头到尾扫一遍数组,每个数只异或一次
- 空间:O(1),只用一个累加器 ans,不开额外数组或哈希表
易错点
面试追问把动画讲成自己的话
追问为什么异或能做到常量空间?
追问如果改成「其余数都出现三次、只有一个出现一次」,异或还行吗?
追问异或运算满足交换律和结合律吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
位 1 的个数
LeetCode 191 · 简单 · 沿着 位运算 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题