1. 问题背景与核心挑战
缺失的第一个正数(First Missing Positive)是LeetCode上一道经典的数组处理题目,编号41,属于热题100中的困难级别。这道题看似简单,却暗藏玄机——它要求我们在时间复杂度O(n)且空间复杂度O(1)的条件下,找出一个未排序整数数组中最小的缺失正整数。
举个例子,给定数组[3,4,-1,1],缺失的第一个正数是2;而[7,8,9,11,12]的答案则是1。这个问题的难点在于严格的时空复杂度限制,排除了排序、哈希表等常规解法,需要我们对数组本身进行"就地"操作。
提示:这道题在亚马逊、微软等大厂面试中频繁出现,因为它能有效考察候选人对数组索引和值之间关系的理解,以及空间复杂度的优化能力。
2. 暴力解法与优化思路
2.1 直观解法及其缺陷
最直观的解法是先将数组排序,然后遍历查找缺失的正数。例如:
def firstMissingPositive(nums): nums.sort() res = 1 for num in nums: if num == res: res += 1 return res这种方法虽然简单,但排序的时间复杂度是O(nlogn),不满足题目要求。另一种思路是使用哈希表存储所有数字,然后从1开始检查是否存在:
def firstMissingPositive(nums): num_set = set(nums) for i in range(1, len(nums)+2): if i not in num_set: return i这种方法虽然时间复杂度是O(n),但空间复杂度也是O(n),同样不符合要求。
2.2 关键突破点:利用数组索引作为哈希
真正的解法需要利用数组本身作为哈希表。核心观察是:对于一个长度为n的数组,缺失的第一个正数一定在[1, n+1]范围内。如果数组包含了1到n的所有数字,那么缺失的就是n+1;否则缺失的一定在1到n之间。
我们可以通过以下步骤实现:
- 首先检查1是否存在于数组中,如果不存在直接返回1
- 将所有非正数和大于n的数替换为1(因为这些数不影响结果)
- 使用数组索引作为哈希键,通过改变符号来标记数字是否存在
- 最后遍历数组,找到第一个正数对应的索引
3. 最优解法的详细实现
3.1 算法步骤拆解
让我们用[3,4,-1,1]这个例子来逐步说明:
- 检查1是否存在:存在,继续
- 替换无效数字:将-1替换为1,数组变为[3,4,1,1]
- 使用索引标记:
- 取绝对值:3 → 将索引3-1=2处的值变为负数:[3,4,-1,1]
- 4 → 索引3(超出范围,忽略)
- 1 → 索引0:[3,4,-1,1](已经是负数)
- 1 → 同上
- 最终数组:[3,4,-1,1]
- 遍历查找第一个正数:索引1的值为4,所以缺失的是2
3.2 Python实现代码
def firstMissingPositive(nums): n = len(nums) # 检查1是否存在 if 1 not in nums: return 1 # 替换无效数字 for i in range(n): if nums[i] <= 0 or nums[i] > n: nums[i] = 1 # 使用索引作为哈希键 for i in range(n): num = abs(nums[i]) if num == n: nums[0] = -abs(nums[0]) else: nums[num] = -abs(nums[num]) # 查找第一个正数 for i in range(1, n): if nums[i] > 0: return i if nums[0] > 0: return n return n + 13.3 复杂度分析
- 时间复杂度:O(n),我们只进行了三次线性遍历
- 空间复杂度:O(1),除了输入数组外没有使用额外空间
4. 边界条件与常见错误
4.1 特殊输入情况
- 空数组:应返回1
- 全负数数组:应返回1
- 连续数组[1,2,3,...,n]:应返回n+1
- 重复数字数组:如[1,1,2,2]应返回3
4.2 常见实现错误
- 忽略1的检查:直接开始标记会导致错误
- 处理n时的特殊逻辑:需要单独处理,因为索引n超出范围
- 重复标记问题:应该取绝对值后再标记,避免重复操作改变符号
- 索引越界:当数字等于数组长度时,需要特殊处理
注意:在面试中,很多候选人会在处理数组长度等于n的数字时出错,这是需要特别注意的边界情况。
5. 算法优化与变种
5.1 交换法替代标记法
除了符号标记法,还可以使用交换法:将每个数字交换到它应该在的位置(即nums[i]应该放在nums[nums[i]-1])。这种方法同样满足O(n)时间和O(1)空间的要求:
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+15.2 相关变种题目
- 找出所有缺失的数字(LeetCode 448)
- 找出重复的数字(LeetCode 287)
- 第一个缺失的非负数
- 缺失的第一个质数
6. 面试技巧与实战建议
6.1 面试回答策略
- 先说明暴力解法及其局限性
- 提出哈希表思路,指出空间复杂度问题
- 引出利用数组本身作为哈希表的想法
- 详细解释标记法的实现步骤
- 讨论边界条件和优化空间
6.2 调试技巧
- 使用小规模测试用例(如[1], [2], [1,1])
- 打印中间结果,观察标记过程
- 特别注意n的处理和索引边界
- 使用断言验证关键步骤
6.3 性能优化
虽然算法已经是理论最优,但在实际实现中还可以:
- 合并某些遍历步骤
- 使用位运算替代符号标记(如果允许修改数据)
- 提前终止条件:如果在标记过程中发现1缺失可以直接返回
7. 实际应用场景
这道算法题虽然抽象,但其核心思想在实际开发中有广泛应用:
- 数据库系统中的缺失ID检测
- 内存管理中的空闲块标记
- 图像处理中的像素值重映射
- 数据压缩算法中的值域优化
理解这种"就地哈希"的技术,可以帮助我们在资源受限的环境下(如嵌入式系统)设计出更高效的算法。