刷力扣刷到 268 这道“缺失数字”时,我第一次感受到位运算的威力——以前总觉得异或是底层语言才需要抠的细节,直到看到最优解只需一行代码就把问题解决了,才意识到自己一直用最笨的方法在刷题。今天把这题从题目本质、多种解法到异或原理一次讲透,尤其适合刚接触位运算、想搞懂“为什么用异或”的读者。
这题在所有大厂笔试和热题清单里出场率都不低,别看它标着 Easy,它背后藏着的数学思维和位运算思想,却能延伸出一整类题型的解法。无论你是刚开始按顺序刷力扣的新手,还是已经在刷力扣热题 100、准备进大厂的老手,花十几分钟把这道题吃透,绝对值回票价。
1. 先看清题目:缺失数字到底在考什么
题目描述很简短:给定一个包含 [0, n] 中 n 个数的数组,找出缺失的那个数。
比如 nums = [3,0,1],因为 [0,3] 应该是 0、1、2、3 四个数,现在数组里只有 3 个,少了 2。
这个题表面上是“找缺失”,本质上考验的是你对数据范围和数学规律的敏感度。数组长度为 n,但值的范围是 0 到 n,这意味着每个元素和下标之间存在天然的对应关系。如果你能抓住这一点,就会明白为什么这题能玩出好几种解法。
很多人第一次看到这题,第一反应是排序,第二反应是哈希,这两种都能过,但都算不上最优。真正让这道题在面试里成为“加分题”的,是位运算异或解法——O(n) 时间、O(1) 空间,不用额外容器、不用担心溢出,一句话解释就能让面试官点头。
我刷题有个习惯:每道 Easy 题都会问自己三个问题——最朴素的做法是什么?为什么它不够好?有没有更优雅的思路?下面我按这个思路,把这道题的解法一层层剥开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 常规思路逐个拆解:三种能过的解法
在讲异或之前,先把常规解法过一遍,不是为了凑字数,而是只有把它们的优缺点看清了,你才能体会异或解法到底妙在哪里。
2.1 排序法:最直觉但最不推荐
思路最简单:先排序,然后遍历数组,如果 nums[i] != i,说明 i 就是缺失的数。如果遍历完都没找到,说明缺失的是 n。
python复制def missingNumber(nums):
nums.sort()
for i in range(len(nums)):
if nums[i] != i:
return i
return len(nums)
复杂度上,时间受排序算法限制,通常是 O(n log n),空间看排序实现,Python 的 Timsort 是 O(n)。
这个解法的问题很明显:题目只让你找一个数,你非要把整个数组排一遍序,做了大量无用功。而且在面试现场,如果你上来就排序,面试官大概率会追问“能不能更快”,到时候还是要回到下面几种思路。
2.2 哈希表法:用空间换时间
把数组中所有数字放进哈希集合,然后从 0 到 n 逐个检查哪个不在集合里。
python复制def missingNumber(nums):
num_set = set(nums)
for i in range(len(nums) + 1):
if i not in num_set:
return i
时间复杂度 O(n),但空间复杂度也是 O(n),因为要额外维护一个哈希集合。
这种解法比排序好,思路也直观,大部分人在面试时能想到这个就已经不错了。但如果你能继续往前走一步,想到异或,这才是区分普通候选人和有算法敏感度的候选人的地方。
2.3 数学求和法:O(1) 空间的聪明办法
在讲位运算之前,数学法是很多“最优解”答案里最常见的一种:
用等差数列求和公式算出 [0, n] 的总和,减去数组中所有数的和,结果就是缺失的数。
python复制def missingNumber(nums):
n = len(nums)
total = n * (n + 1) // 2
return total - sum(nums)
时间复杂度 O(n)(求和要遍历一次数组),空间复杂度 O(1)。
看起来已经挺优了,代码也简洁。但它有一个隐患:当 n 很大时,n * (n + 1) 可能溢出。在大厂面试里,面试官很喜欢抓着这一点深挖:“如果 n 是 10^9 甚至更大,求和会不会有问题?”一旦你用求和法,就相当于把这个问题递到了面试官嘴边。
当然,Python 的大整数不会溢出,但如果你用 C++ 或 Java 写,int 溢出就是实打实的问题。这一点我们后面会专门展开说。
3. 异或解法的核心原理与完整推导
3.1 异或运算的三条关键性质
异或(XOR)是一种位运算,符号是 ^,运算规则很简单:两个二进制位相同结果为 0,不同结果为 1。但真正让它强大的,是下面三条性质:
- 归零律:
x ^ x = 0,任何数和自己异或,结果都是 0。 - 恒等律:
x ^ 0 = x,任何数和 0 异或,结果还是它自己。 - 交换律和结合律:
a ^ b ^ c = a ^ c ^ b,运算顺序不影响结果。
这三条性质放在一起,能推出一个非常重要的结论:同一批数异或两次会相互抵消。
举个简单例子:5 ^ 3 ^ 5,根据交换律变成 5 ^ 5 ^ 3,前两个抵消变 0,结果就是 0 ^ 3 = 3。
这正是“找缺失”问题的核心钥匙。在 268 这道题里,如果我们把 [0, n] 的所有整数和数组里的所有数放在一起异或,那么数组里存在的数会出现两次,直接抵消成 0;只有缺失的那个数只出现一次,最终留下的就是它。
3.2 手把手推演一遍:用 [3,0,1] 验证
拿题目示例 nums = [3, 0, 1] 走一遍完整流程。
首先,n = 3,完整区间是 [0, 1, 2, 3],数组是 [3, 0, 1],缺失的是 2。
我们将所有数拉出来异或:
code复制0 ^ 1 ^ 2 ^ 3 ^ 3 ^ 0 ^ 1
用交换律重新排列:
code复制0 ^ 0 ^ 1 ^ 1 ^ 3 ^ 3 ^ 2
两个 0 抵消、两个 1 抵消、两个 3 抵消,剩下的就是:
code复制0 ^ 0 ^ 0 ^ 2 = 2
缺失的那个数自己是“落单”的,异或结果永远是它。
这个思路妙在哪里?它不需要排序、不需要额外容器、不需要求和,只需要一个变量从头异或到尾,时间和空间都被压到极致。而且它完全不怕溢出——异或只处理二进制位,没有进位,自然不存在溢出问题。
3.3 为什么异或解法是这道题的最优解
算法题里的“最优”从来不是绝对的,而是在“时间、空间、代码简洁度、面试表达难度”这四个维度上综合取舍。
对比一下四种解法:
| 解法 | 时间复杂度 | 空间复杂度 | 核心风险 |
|---|---|---|---|
| 排序法 | O(n log n) | O(1) 或 O(n) | 效率低,做了无用功 |
| 哈希表 | O(n) | O(n) | 空间浪费 |
| 数学求和 | O(n) | O(1) | 大数溢出风险 |
| 异或 | O(n) | O(1) | 需要理解位运算,入门门槛稍高 |
异或解法在时间上和其他两种 O(n) 解法持平,空间上与求和法持平,但它额外解决了溢出问题,而且代码同样简洁。更重要的是,在面试现场,能写出异或解法的人,往往能体现自己对底层运算的理解深度,这在候选人中间是一个很有辨识度的亮点。
4. 代码实现与边界情况全梳理
4.1 C++ 实现:面试最常写的版本
C++ 是很多大厂面试的首选语言,这道题用 C++ 写异或解法,简洁得让人舒服:
cpp复制class Solution {
public:
int missingNumber(vector<int>& nums) {
int ans = 0;
for (int i = 0; i < nums.size(); i++) {
ans ^= i;
ans ^= nums[i];
}
ans ^= nums.size();
return ans;
}
};
循环里同时异或下标和对应元素,最后再补上 n 这个下标。因为数组长度为 n,循环里下标只走到 n-1,但完整区间 [0, n] 里包含 n,所以循环结束后要额外异或一次 nums.size()。
有人可能会问:为什么要初始化 ans = 0?因为 0 是异或的“单位元”,任何数和 0 异或都等于它本身,从 0 开始不会影响任何结果。这在位运算里相当于加法里的“从 0 开始加”一样自然。
4.2 Python 实现:一行代码的最简写法
如果你用 Python 刷题,直接用 reduce 可以写成一行:
python复制from functools import reduce
def missingNumber(nums):
return reduce(lambda x, y: x ^ y, list(range(len(nums) + 1)) + nums)
把 [0, n] 完整区间和原数组合并成一个列表,然后逐个异或,reduce 会自动完成全部抵消。跑起来没有任何问题,但面试时不太建议一上来就写这种一行流——万一面试官问你 reduce 的底层逻辑,你解释起来反而绕。
更推荐下面这种思路清晰的写法:
python复制def missingNumber(nums):
ans = 0
for i, num in enumerate(nums):
ans ^= i
ans ^= num
ans ^= len(nums)
return ans
这个写法把循环里的逻辑拆成两行,每一行都有一一对应的语义,面试时更好讲,写起来也不容易出错。
4.3 边界情况与常见错误清单
根据我刷题的踩坑经验,这题有四个边界情况值得单独说:
第一个是 n = 0 的情况。 数组为空,缺失的数就是 0。上面的代码走一遍:循环不执行,ans ^= 0,结果还是 0,正确。
第二个是缺失 n 本身。 比如 nums = [0, 1],n = 2,缺失的是 2。很多人会忘记循环结束后还要异或一次 n,导致返回 0。这就是为什么我在前面代码里特别强调“循环结束后补上 nums.size()”,这道题的“坑”多数埋在这里。
第三个是数组长度为 1 的情况。 nums = [0],缺失的是 1。循环只跑一次,ans = 0 ^ 0 ^ 0 = 0,再补上长度 1,0 ^ 1 = 1,正确。
第四个是不同语言里整数范围的问题。 异或解法对整数范围完全免疫,但如果你用求和法,在 C++ 里 int 溢出会直接导致答案错误。这一点在面试里值得主动提出来,能让面试官觉得你考虑问题很周全。
4.4 代码优化:能不能更进一步
还有一个很常见的变体写法,用“一个变量同时异或”的方式减少一次赋值:
cpp复制class Solution {
public:
int missingNumber(vector<int>& nums) {
int ans = nums.size();
for (int i = 0; i < nums.size(); i++) {
ans ^= i ^ nums[i];
}
return ans;
}
};
这里把 ans 初始化为 n,循环里直接异或下标和元素,实现同样效果,但代码更紧凑。两种写法没有本质区别,选自己不容易写错的那种就好。
5. 位运算思路在面试和刷题中的实际价值
5.1 从 268 延伸出去的题目族
力扣里有很多题目用的都是同一套异或思想,刷完 268 之后,我强烈建议趁热打铁把这些题一起刷了,你会发现“异或抵消”这一招几乎是通用的:
- 136. 只出现一次的数字:数组中只有一个数出现一次,其他都出现两次。这题直接用异或把所有数过一遍,落单的就是答案,是 268 的最佳续集。
- 260. 只出现一次的数字 III:有两个落单的数,需要先整体异或拿到两个数的差异位,再分组异或。这道题把异或思想从“找单个数”升级到“找两个数”,难度直接上一个台阶。
- 371. 两整数之和:用异或模拟无进位加法,再用与运算和左移处理进位。这题能让你彻底吃透位运算的底层逻辑。
这几题刷下来,你对异或的敏感度会发生质变,以后看到“出现一次”“缺失”“重复”这类字眼,第一反应就会想到位运算,而不是排序或哈希。
5.2 什么情况下应该优先考虑位运算
根据我的刷题经验,遇到下面这几类特征时,位运算值得优先尝试:
- 数据范围是连续整数,这道题要求从 [0, n] 中找缺失,天然适合异或。
- 只要求找一个结果,而不是找多个或找全部。
- 空间复杂度卡得很死,明确要求 O(1) 空间,哈希表被排除。
- 题目提到“出现偶数次”“出现一次”“配对”,这类词基本就是位运算的信号。
当然,位运算不是万能的。如果题目要求返回的是“缺失数字的个数”或“所有缺失的数字”,异或就没那么直接了,可能需要配合其他手段。判断一种技巧能不能用,关键在于题目的约束条件,而不是“看起来像不像位运算”。
5.3 刷题策略:一道题的价值在于串联知识
既然提到刷题顺序和刷题攻略,我多说一句:很多人刷题有个误区,按题号顺序从 1 开始刷,或者只挑 Hard 题证明自己,结果刷得很累,知识还是零散的。
我的建议是“按知识簇刷题”。比如今天学异或,就把 136、268、260、371 放一起刷;学滑动窗口,就把 3、76、424、209 放一起。这样每刷一组,你对一类思想的掌握就加深一层。刷完 268 之后,我特意去找了上面那几道位运算题,用两个晚上集中刷完,效果比分散刷一个月还好。
另外,进大厂刷力扣,重点不是数量而是理解深度。一道 Easy 题能答出最优解、说清原理、主动指出边界情况,并且在面试官的追问下不掉链子,比闷头刷 500 道题更有说服力。
6. 面试表达:怎么把异或解法讲出高分
这部分是很多人忽略的。会写代码和会讲代码是两回事,面试官考察的不只是你能不能 AC,更是你能不能把思路讲清楚。
我建议按照“动机 → 原理 → 推导 → 复杂度 → 边界”五步法来呈现这道题:
先说动机:“我注意到这题的数据是 [0, n] 区间内缺一个数,这种配对题目通常可以用异或来处理。”这句话一出来,面试官就知道你有备而来。
再讲原理:“异或有两个关键性质,x ^ x = 0 和 x ^ 0 = x。我把数组里的所有数和完整区间里的所有数放一起异或,出现两次的数都会抵消成 0,最后剩下的就是缺失的数。”
接着做一步推导演示,拿一个小数组在纸上或白板上画一下抵消过程,让面试官看到你不是背答案。
然后说复杂度:“时间 O(n),需要一次遍历;空间 O(1),只用一个变量。相比求和法,异或不会溢出。”
最后主动提边界:“如果缺失的是 n 本身,循环结束后要额外异或一次数组长度,否则会返回错误结果。数组长度为 0 时,答案就是 0。”
这套话术我整理成了自己的笔记,后来面试时遇到类似题目,直接套用,基本上都能在“解题思路清晰”这一项拿到好评。
7. 一个容易被忽略的细节:为什么初始化是 0 而不是 1
最后我想分享一个我在学习中踩过的坑。一开始我写这段代码时,想的是“从第一个数开始异或就行了”,于是把 ans 初始化成了 nums[0],但这样写会出错。
原因在于:如果直接把 ans 初始化为 nums[0],你就跳过了和下标 0 配对这一步。而这道题的精髓恰恰是“下标和值一一配对再抵消”,你跳过任何一个配对,就破坏了整个抵消过程。
所以正确的做法一定是 ans = 0 开始,循环里依次异或每个 i 和每个 nums[i],最后再补上 nums.size()。这样完整区间 [0, n] 里的每一个数都参与了一次异或,不多不少,恰好只留下缺失值。
刷题的意义就在于此:一道 Easy 题,表面上只是找一个缺失的数,背后却串联起哈希、数学、位运算三条完全不同的解题路径。而位运算这条路径,恰恰是大多数人刷题时最容易忽略、却在关键时刻最能体现功力的。把这道题彻底搞懂,收获的绝不仅仅是一个 AC,而是面对“配对、出现一次、缺失、重复”这类问题时,多了一把最锋利的解题尖刀。
