1. 题目解析与核心思路
LeetCode 41题"缺失的第一个正数"是一道经典的数组处理问题,题目要求找出一个未排序整数数组中没有出现的最小正整数。这道题被收录在热门题库Hot100中,考察的是对数组的原地操作能力和对时间/空间复杂度的优化意识。
1.1 问题描述重述
给定一个包含n个整数的数组nums,我们需要找出其中没有出现的最小正整数。要求算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。例如:
- 输入:[1,2,0] → 输出:3
- 输入:[3,4,-1,1] → 输出:2
- 输入:[7,8,9,11,12] → 输出:1
1.2 关键约束条件分析
这道题的两个关键约束条件决定了我们不能使用常规的排序或哈希表解法:
- 时间复杂度O(n):排除了所有基于比较的排序算法(通常为O(nlogn))
- 空间复杂度O(1):排除了使用哈希表等额外数据结构的可能
1.3 解题思路突破点
经过分析,我们可以得出以下关键观察:
- 缺失的第一个正数一定在[1, n+1]范围内(n为数组长度)
- 我们可以利用数组本身作为哈希表,通过原地修改数组来标记数字是否存在
- 需要处理数组中的非正数和大于n的数
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法实现与代码解析
2.1 算法步骤详解
-
预处理阶段:
- 首先检查数组是否包含1,如果没有则直接返回1
- 将所有非正数和大于n的数替换为1(因为这些数不影响结果)
-
哈希标记阶段:
- 遍历数组,对于每个元素nums[i],将nums[abs(nums[i])-1]标记为负数
- 这样数组下标+1对应的数字存在时,该位置会被标记为负
-
结果检查阶段:
- 再次遍历数组,第一个正数所在的位置i+1就是缺失的最小正数
- 如果全部为负数,则返回n+1
2.2 Python实现代码
python复制def firstMissingPositive(nums):
n = len(nums)
# 检查1是否存在
if 1 not in nums:
return 1
# 预处理:将所有非正数和大于n的数替换为1
for i in range(n):
if nums[i] <= 0 or nums[i] > n:
nums[i] = 1
# 使用数组本身作为哈希表
for i in range(n):
a = abs(nums[i])
if a == n:
nums[0] = -abs(nums[0])
else:
nums[a] = -abs(nums[a])
# 检查第一个正数的位置
for i in range(1, n):
if nums[i] > 0:
return i
if nums[0] > 0:
return n
return n + 1
2.3 关键代码解释
-
预处理阶段:
- 将无效数字替换为1是为了保证后续标记阶段不会越界
- 这个操作不会影响结果,因为这些数字本身就不在候选答案范围内
-
哈希标记阶段:
- 使用绝对值是为了避免重复标记导致的符号反转
- 特殊处理n的情况(因为数组长度为n,索引最大为n-1)
-
结果检查阶段:
- 从1开始检查是因为0位置用于存储n的信息
- 最后检查nums[0]是因为n可能没有被标记
3. 复杂度分析与优化
3.1 时间复杂度分析
算法总共进行了3次线性遍历:
- 检查1是否存在:O(n)
- 预处理阶段:O(n)
- 哈希标记阶段:O(n)
- 结果检查阶段:O(n)
总时间复杂度为O(n),满足题目要求。
3.2 空间复杂度分析
除了输入数组外,算法只使用了常数个额外变量,因此空间复杂度为O(1),满足题目要求。
3.3 可能的优化方向
-
合并遍历次数:
- 可以将检查1存在和预处理合并为一次遍历
- 但这不会改变时间复杂度的大O表示
-
边界条件优化:
- 在预处理阶段可以同时记录最小正数,可能提前终止
-
代码可读性优化:
- 可以将不同阶段封装为独立函数提高可读性
4. 常见错误与调试技巧
4.1 典型错误案例
-
忽略1的特殊情况:
python复制# 错误示例:没有先检查1是否存在 def firstMissingPositive(nums): n = len(nums) for i in range(n): if nums[i] <= 0: nums[i] = n + 1 # 可能导致错误结果 -
索引越界问题:
python复制# 错误示例:没有处理nums[i]=n的情况 nums[abs(nums[i])] = -abs(nums[abs(nums[i])]) # 当nums[i]=n时会越界 -
符号处理不当:
python复制# 错误示例:没有使用绝对值 nums[nums[i]] = -nums[nums[i]] # 如果nums[nums[i]]已经是负数会被反转回来
4.2 调试技巧
-
小规模测试用例:
- 先测试边界情况:[1], [2], [1,1]
- 再测试常规情况:[3,4,-1,1], [1,2,0]
-
打印中间结果:
python复制print(f"预处理后: {nums}") print(f"标记后: {nums}") -
可视化追踪:
- 在纸上画出数组每一步的变化
- 特别关注符号变化和索引对应关系
5. 同类问题扩展
5.1 相似题目推荐
-
LeetCode 268. 丢失的数字:
- 更简单的版本,可以使用数学方法解决
-
LeetCode 448. 找到所有数组中消失的数字:
- 使用类似的原地标记方法
- 需要找出所有缺失的数字
-
LeetCode 287. 寻找重复数:
- 同样有O(n)时间和O(1)空间的限制
- 可以使用快慢指针法
5.2 变种问题思考
-
如果允许使用O(n)空间:
- 可以直接使用哈希表存储所有数字
- 然后从1开始检查第一个不在哈希表中的数
-
如果数组是只读的:
- 需要使用其他方法如二分查找
- 时间复杂度会变为O(nlogn)
-
找出前k个缺失的正数:
- 需要扩展当前算法
- 可能需要在标记阶段记录更多信息
6. 实际应用场景
这种类型的算法在以下场景中有实际应用:
-
数据库系统:
- 寻找可用的最小ID号
- 维护连续的主键序列
-
内存管理:
- 寻找可用的最小内存块编号
- 维护资源分配表
-
序列号生成:
- 确保发放的序列号连续无缺失
- 检测序列中的缺失号码
7. 个人解题心得
在解决这道题的过程中,我总结了以下几点经验:
-
充分利用约束条件:
- 空间限制提示我们需要原地操作
- 时间限制提示我们需要线性解法
-
善用数组下标信息:
- 数组的下标本身就是一种隐含的信息
- 可以通过操作元素来存储额外信息
-
分阶段处理问题:
- 先处理简单情况(如1不存在)
- 再处理一般情况
- 最后处理边界情况
-
测试驱动开发:
- 先写出测试用例再编写代码
- 特别关注边界条件的测试
这道题的巧妙之处在于利用数组本身作为哈希表,通过符号位来存储额外信息。这种"原地哈希"的技巧在很多类似问题中都有应用,掌握这个技巧可以解决一系列相关问题。
