1. 问题描述与数学解法解析
今天咱们来聊聊LeetCode上第268题"丢失的数字"。题目很简单:给定一个包含[0, n]中n个数的数组nums,找出[0, n]这个范围内没有出现在数组中的那个数。比如输入[3,0,1],输出应该是2,因为0、1、3都在,唯独缺了2。
这道题最直观的解法当然是排序后遍历查找,时间复杂度O(nlogn)。但更巧妙的是数学解法——利用求和公式。我们知道0到n的连续整数和可以用高斯公式计算:(n*(n+1))/2。而数组nums的和就是所有存在的数字之和。两者相减,自然就得到缺失的那个数。
注意:这个方法的前提是数字确实是从0到n连续且只缺一个,题目已经保证了这一点。
2. 代码实现与优化
先看基础实现版本:
java复制class Solution {
public int missingNumber(int[] nums) {
int sum = 0;
int n = nums.length;
for (int num : nums) {
sum += num;
}
return (n * (n + 1)) / 2 - sum;
}
}
这个实现有几个值得注意的点:
- 使用增强for循环遍历数组,代码更简洁
- 直接使用高斯公式计算理论总和,避免第二个循环
- 时间复杂度O(n),空间复杂度O(1),已经是最优解
但这段代码有个潜在问题:当n很大时,(n*(n+1))/2可能会整数溢出。比如n=65535时,(65535*65536)/2=2147450880,还在int范围内;但当n=65536时就会溢出。
改进方案:
java复制class Solution {
public int missingNumber(int[] nums) {
int res = nums.length;
for (int i = 0; i < nums.length; i++) {
res += i - nums[i];
}
return res;
}
}
这个版本更巧妙:
- 初始res设为n
- 遍历时用i(应该有的数)减去numsi
- 累加这些差值,最终结果就是缺失的数
- 完全避免了乘法运算,不会溢出
3. 数学原理深入解析
这个解法的数学本质是利用了等差数列的性质。0到n的连续整数是一个公差为1的等差数列,其和S=n*(n+1)/2。
当缺少一个数x时,数组和S'=S-x。因此x=S-S'。
更深层的数学原理是:在模运算中,求和是一种"弱哈希",可以检测出单个元素的差异。类似的思想在密码学和校验和中很常见。
这个解法之所以高效,是因为:
- 求和操作时间复杂度O(n),是最低限度的
- 不需要额外空间
- 利用了数学性质,避免暴力搜索
4. 边界条件与测试用例
好的代码必须考虑各种边界情况:
-
缺失0的情况:
输入:[1,2,3]
输出:0 -
缺失最后一个数的情况:
输入:[0,1,2]
输出:3 -
单个元素缺失0:
输入:[1]
输出:0 -
单个元素缺失1:
输入:[0]
输出:1 -
大数测试(确保不溢出):
输入:大数组缺少中间某个数
在面试中,主动提出这些测试用例能展现你的全面思考。
5. 其他解法对比
除了数学求和法,还有几种常见解法:
哈希表法:
java复制class Solution {
public int missingNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) set.add(num);
for (int i = 0; i <= nums.length; i++) {
if (!set.contains(i)) return i;
}
return -1;
}
}
- 时间复杂度O(n)
- 空间复杂度O(n)
- 优点:思路直接
- 缺点:需要额外空间
位运算法(异或):
java复制class Solution {
public int missingNumber(int[] nums) {
int res = nums.length;
for (int i = 0; i < nums.length; i++) {
res ^= i ^ nums[i];
}
return res;
}
}
- 利用x^x=0的性质
- 所有成对出现的数异或后为0,剩下的就是缺失的数
- 同样O(n)时间,O(1)空间
- 比求和法稍难理解,但更通用(不担心溢出)
6. 实际应用场景
这类问题在实际开发中其实很常见,比如:
- 检查连续的ID中是否有缺失
- 验证数据完整性
- 分布式系统中检测缺失的消息序号
- 数据库记录连续性检查
理解这类算法有助于处理各种数据完整性问题。比如在支付系统中,我们需要确保交易序号没有缺失或重复。
7. 常见错误与调试技巧
新手容易犯的几个错误:
-
忘记处理缺失0的情况:
- 错误做法:只从1开始求和
- 正确做法:必须包含0
-
循环边界错误:
- 错误:for(int i=0;i<n;i++)计算总和
- 正确:应该是i<=n或者i<n+1
-
整数溢出:
- 大n时n*(n+1)可能溢出
- 解决方法:使用long或者改进算法
调试技巧:
- 打印中间结果(sum值)
- 用小的测试用例手动验证
- 特别注意n=0, n=1的边界情况
8. 算法复杂度分析
数学求和法:
- 时间复杂度:O(n),必须遍历整个数组一次
- 空间复杂度:O(1),只用了常数个额外变量
这是最优解,因为:
- 任何算法至少需要O(n)时间读取输入
- 不需要额外空间
相比之下,排序法需要O(nlogn)时间,哈希表法需要O(n)空间。
9. 语言特性与实现细节
Java实现时要注意:
-
增强for循环比传统for循环更简洁:
java复制for(int num : nums) vs for(int i=0;i<nums.length;i++) -
整数除法特性:
- n*(n+1)保证是偶数,所以除以2不会丢失精度
- 但最好还是先乘后除,避免浮点运算
-
流式API实现(Java 8+):
java复制int sum = Arrays.stream(nums).sum();虽然简洁,但效率略低
10. 扩展思考
这个问题可以有几个变种:
-
如果缺少两个数字怎么办?
- 解法:可以用方程组求解
- 先求和得到x+y的值
- 再求平方和得到x²+y²的值
- 解这个方程组
-
如果数组可能包含重复怎么办?
- 需要先用哈希表统计频率
- 然后找出频率为0的数
-
如果数组已排序怎么办?
- 可以用二分查找优化到O(logn)时间
这类问题的核心思想是:利用数学性质将搜索问题转化为计算问题,从而降低时间复杂度。这也是算法设计的精髓之一——找到问题的本质特征,用最合适的方法解决。