1. 问题背景与核心挑战
缺失的第一个正数(First Missing Positive)是力扣(LeetCode)平台上的一道经典算法题,编号为41。这道题被收录在力扣热题100(Hot 100)中,属于数组类问题的典型代表。题目要求我们找出一个未排序整数数组中没有出现的最小的正整数。
这个问题的难点在于其时间复杂度要求为O(n),空间复杂度要求为O(1)。这意味着我们不能使用常规的排序方法(如快速排序的O(nlogn)),也不能使用哈希表等额外空间(O(n)空间复杂度)。这种限制条件迫使我们必须在原数组上进行操作,通过巧妙的元素交换和标记来实现目标。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解题思路
2.1 问题重述
给定一个未排序的整数数组nums,找出其中没有出现的最小的正整数。例如:
- 输入:[1,2,0] 输出:3
- 输入:[3,4,-1,1] 输出:2
- 输入:[7,8,9,11,12] 输出:1
2.2 关键观察点
解决这个问题的关键在于几个重要观察:
- 缺失的第一个正数一定在1到n+1之间(n为数组长度)
- 我们可以利用数组本身来记录哪些数字已经出现
- 通过将数字放到它"应该"在的位置,我们可以重构数组的信息
2.3 基本思路
基于上述观察,我们可以采用以下步骤:
- 首先处理所有非正数(将它们设为n+1)
- 遍历数组,对于每个元素,将其对应的位置标记为负
- 最后扫描数组,第一个正数所在的位置就是缺失的最小正数
3. 详细解法与代码实现
3.1 解法一:原地哈希法
这是最符合题目要求的解法,时间复杂度O(n),空间复杂度O(1)。
python复制def firstMissingPositive(nums):
n = len(nums)
# 第一步:将所有非正数标记为n+1
for i in range(n):
if nums[i] <= 0:
nums[i] = n + 1
# 第二步:使用负号标记存在的数字
for i in range(n):
num = abs(nums[i])
if num <= n:
nums[num - 1] = -abs(nums[num - 1])
# 第三步:找到第一个正数的位置
for i in range(n):
if nums[i] > 0:
return i + 1
# 如果1到n都存在,返回n+1
return n + 1
3.2 解法二:交换位置法
另一种思路是通过交换元素到正确的位置:
python复制def firstMissingPositive(nums):
n = len(nums)
for i in range(n):
# 不断交换,直到当前元素在正确位置或无法交换
while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
# 扫描找出第一个不符合的位置
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1
4. 算法复杂度分析
4.1 时间复杂度
两种解法的时间复杂度都是O(n)。虽然解法二中有嵌套循环,但每个元素最多被交换一次,因此总体时间复杂度仍然是线性的。
4.2 空间复杂度
两种解法都只使用了常数级别的额外空间,满足O(1)的要求。
5. 边界条件与测试用例
5.1 常见边界情况
- 空数组:应返回1
- 全负数数组:应返回1
- 连续正数数组:[1,2,3]应返回4
- 包含重复元素的数组:[1,1]应返回2
5.2 测试用例设计
python复制test_cases = [
([1,2,0], 3),
([3,4,-1,1], 2),
([7,8,9,11,12], 1),
([], 1),
([1], 2),
([1,1], 2),
([2,1], 3),
([1,2,3,4,5], 6)
]
6. 实际应用与变种问题
6.1 实际应用场景
这个问题虽然看起来简单,但在实际中有重要应用:
- 数据库系统中的连续ID分配
- 内存管理中的空闲块查找
- 序列号生成系统中的空缺检测
6.2 变种问题
- 找出缺失的所有正数
- 找出第一个缺失的非负整数(包含0)
- 流式数据中的第一个缺失正数(无法存储全部数据)
- 允许使用额外空间时的解法比较
7. 解题技巧与优化建议
7.1 解题技巧
- 对于数组类问题,考虑是否可以在原数组上进行标记
- 利用数组索引本身作为信息存储位置
- 使用正负号或特殊值来标记信息是常见技巧
- 注意处理重复元素的情况
7.2 优化建议
- 在解法二中,可以添加提前终止条件
- 对于特别大的数组,可以考虑分块处理
- 在实际应用中,可以结合具体场景进行针对性优化
8. 常见错误与调试技巧
8.1 常见错误
- 忽略非正数的处理
- 索引越界(如nums[i]-1可能为负)
- 重复元素的处理不当
- 边界条件考虑不周(如全正数数组)
8.2 调试技巧
- 打印数组在每一步的变化
- 使用小规模测试用例手动验证
- 特别注意循环不变量的维护
- 检查每个元素的最终位置是否正确
9. 力扣热题100中的相关题目
在力扣Hot 100中,有几个与本题相关的题目值得一起练习:
- 两数之和(Two Sum)
- 盛最多水的容器(Container With Most Water)
- 移动零(Move Zeroes)
- 找到所有数组中消失的数字(Find All Numbers Disappeared in an Array)
10. 个人解题心得
在实际解决这个问题时,我最初尝试了排序法,但发现不满足复杂度要求。后来通过分析问题特性,意识到可以利用数组本身来存储信息。解法二的交换位置法虽然代码更简洁,但在理解上需要更多思考。建议初学者先从解法一的标记法入手,理解后再尝试解法二。
一个容易忽略的点是重复元素的处理。在解法二中,如果不添加nums[nums[i]-1] != nums[i]的条件,可能会导致无限循环。这也是为什么在面试中,面试官常常会追问如何处理重复元素的原因。
