LeetCode 268. 丢失的数字
题目描述


题意分析
输入是一个长度为
n的数组,里面装着[0, n]这n + 1个整数中的n个,且互不重复,要求输出唯一缺席的那一个。注意范围上界是n而不是n - 1:数组有n个坑,候选值却有n + 1个,正好差一个,缺失值唯一。约束里最重要的信号有三条。第一,元素互不相同,所以不存在「同一个数出现两次挤掉别人」的干扰,缺失的恰好一个。第二,值域被死死钉在
[0, n],n又等于数组长度,这意味着「应该有哪些数」是完全已知的,问题从「搜索」退化成「已知全集减去已有集合」。第三,题目进阶明确要求线性时间、常数额外空间,等于直接封杀了排序($O(n \log n)$)和哈希集合($O(n)$ 空间)这两条最省事的路。边界上要注意:数组可能只有一个元素,此时答案要么是
0要么是1;缺失值可能是范围的两个端点0或n,很多写法恰恰在这两处翻车;数组本身无序,不能假设nums[i]和下标有任何对应关系。
解法:异或抵消
核心思路
候选全集是
0..n,数组中除缺失值外,每个候选数都恰好出现一次。把全集和数组一起异或,利用x ^ x = 0、x ^ 0 = x以及交换律、结合律,所有出现两次的数都会抵消,最后只剩缺失值。实现时令
ans = n,循环中同时异或下标i和元素nums[i]。下标覆盖0..n-1,再加上初值n,正好组成完整候选集,无需额外一趟循环。循环不变量:处理完前
i个元素后,ans等于n、下标0..i-1和数组元素nums[0..i-1]的异或结果。循环结束时,每个非缺失值分别来自候选集和数组,成对抵消;缺失值只来自候选集,因此ans就是答案。相比求和法,异或不会产生更大的中间值,也就没有整数溢出问题。
解题步骤
- 设
n = nums.length,初始化ans = n,先补上候选集中唯一没有对应下标的n。- 遍历数组,对每个位置执行
ans ^= i和ans ^= nums[i]。- 遍历结束后返回
ans。以
[3,0,1]为例,初始ans = 3,依次合入(0,3)、(1,0)、(2,1)后得到2。候选集中的0、1、3都和数组中的同值元素抵消,只有缺失的2留下。
代码实现
class Solution {
public int missingNumber(int[] nums) {
int ans = nums.length;
for (int i = 0; i < nums.length; i++) {
// 下标和数组值中成对出现的数字会被异或抵消。
ans ^= i;
ans ^= nums[i];
}
return ans;
}
}
func missingNumber(nums []int) int {
ans := len(nums)
for i, num := range nums {
// 只出现一次的那个范围数字就是缺失值。
ans ^= i
ans ^= num
}
return ans
}
复杂度分析
- 时间复杂度:$O(n)$,只遍历数组一次。
- 空间复杂度:$O(1)$,只使用常数个变量。
关键点总结
- 将候选全集与实际数组做异或,成对值抵消,缺失值落单。
ans必须初始化为n,因为循环下标只能提供0..n-1。- 数组无须有序;异或的交换律和结合律保证遍历顺序不影响结果。
- 面试中可以对比求和法:两者都是 $O(n)$ 时间、$O(1)$ 空间,但异或没有中间和溢出的风险。
易错点总结
- 把
ans初始化为0会漏掉候选值n;如[0,1]的答案应为2。- 只异或数组值、不异或下标,无法构造完整候选集。
- 写成
ans = i ^ nums[i]会覆盖此前结果,必须使用累计异或^=。- 题目没有保证数组有序,不能直接用
nums[i] == i做二分判断。- 若改用求和法,扩展到更大数据范围时应使用足够宽的整数类型,避免
n * (n + 1)先溢出。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 面试题 17.04. 消失的数字 | 简单 | 同款异或抵消求单个缺失 |
| 面试题 17.19. 消失的两个数字 | 困难 | 按低位分组分离两个缺失值 |
| 136. 只出现一次的数字 | 简单 | 成对元素中挑出落单元素 |
| 剑指 Offer 53 - II. 0~n-1中缺失的数字 | 简单 | 有序前提下二分定位断点 |
| 448. 找到所有数组中消失的数字 | 简单 | 原地打负号标记求多个缺失 |
| 645. 错误的集合 | 简单 | 同时求出重复值与缺失值 |
| 41. 缺失的第一个正数 | 困难 | 值域无界时的原地哈希置换 |