1. 问题定义与核心挑战
"寻找缺失的最小正整数"(Find the Smallest Missing Positive Integer)是算法面试中的经典问题,看似简单却暗藏玄机。给定一个包含整数的无序数组,我们需要找到其中缺失的最小正整数。例如对于数组[3, 4, -1, 1],缺失的最小正整数是2;而对于[7, 8, 9, 11, 12],结果则是1。
这个问题的特殊之处在于:
- 它不关心负数和零(因为它们不算是"正整数")
- 它不关心大于数组长度的数(因为缺失的最小正整数必然在1到n+1之间)
- 它要求时间复杂度O(n)且空间复杂度O(1)(这才是真正的挑战)
2. 暴力解法与优化思路
2.1 直观的暴力解法
最直接的思路是从1开始逐个检查每个正整数是否存在于数组中:
def firstMissingPositive(nums): for i in range(1, len(nums)+2): if i not in nums: return i这种方法虽然简单,但时间复杂度是O(n²)(因为in操作需要O(n)时间),空间复杂度O(1)。在面试中提出这个解法通常只能得到基础分。
2.2 哈希表优化
我们可以用哈希表来存储所有数字,将查找时间降到O(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)。这已经是不错的解法,但面试官往往会追问:"能否在不使用额外空间的情况下解决?"
3. 原地哈希的精妙解法
3.1 核心思路
要达到O(n)时间和O(1)空间,我们需要利用数组本身作为哈希表。具体思路是:
- 遍历数组,将每个正整数x放到它应该在的位置(即索引x-1处)
- 再次遍历数组,第一个位置不正确的索引i对应的i+1就是缺失的最小正整数
3.2 实现细节
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+13.3 关键点解析
while循环中的交换操作确保每个数字最终都在正确位置- 条件
1 <= nums[i] <= n确保我们只处理可能的正整数 - 条件
nums[nums[i]-1] != nums[i]避免无限循环
注意:这个解法会修改原数组,如果题目要求不能修改输入,则需要使用其他方法。
4. 边界情况与测试用例
4.1 常见边界情况
- 空数组:应返回1
- 全负数数组:应返回1
- 连续正整数数组:如[1,2,3]应返回4
- 包含重复数字的数组:如[1,1]应返回2
4.2 测试用例示例
test_cases = [ ([], 1), ([1,2,0], 3), ([3,4,-1,1], 2), ([7,8,9,11,12], 1), ([1,1], 2), ([2,1], 3) ] for nums, expected in test_cases: assert firstMissingPositive(nums.copy()) == expected5. 算法复杂度分析
5.1 时间复杂度
虽然代码中有嵌套循环,但每个数字最多被交换到正确位置一次,因此总时间复杂度是O(n)。
5.2 空间复杂度
除了几个临时变量外,没有使用额外空间,因此空间复杂度是O(1)。
6. 实际应用场景
这个问题虽然抽象,但其解法思想在实际中有重要应用:
- 数据库系统中的缺失ID检测
- 连续编号系统中的空缺位置查找
- 内存管理中的空闲块查找
- 分布式系统中的序列号完整性检查
7. 变种问题与扩展思考
7.1 变种问题
- 找出所有缺失的正整数
- 找出第一个缺失的非负整数(包含0)
- 流式数据中的缺失正整数查找(无法存储整个数组)
7.2 扩展思考
- 如果允许O(n)空间但要求单次遍历,如何解决?
- 如果数组已经排序,是否有更优解法?
- 如何将这个算法并行化处理超大数组?
8. 编码实现中的常见陷阱
- 索引越界:在处理
nums[nums[i]-1]时要确保索引有效 - 无限循环:交换时需要检查是否已经处于正确位置
- 重复数字:需要正确处理重复数字的情况
- 负数处理:要跳过负数和零,但不要误判
9. 不同语言的实现差异
9.1 Java实现
public int firstMissingPositive(int[] nums) { int n = nums.length; for (int i = 0; i < n; i++) { while (nums[i] > 0 && nums[i] <= n && nums[nums[i]-1] != nums[i]) { int temp = nums[nums[i]-1]; nums[nums[i]-1] = nums[i]; nums[i] = temp; } } for (int i = 0; i < n; i++) { if (nums[i] != i+1) return i+1; } return n+1; }9.2 C++实现
int firstMissingPositive(vector<int>& nums) { int n = nums.size(); for (int i = 0; i < n; i++) { while (nums[i] > 0 && nums[i] <= n && nums[nums[i]-1] != nums[i]) { swap(nums[nums[i]-1], nums[i]); } } for (int i = 0; i < n; i++) { if (nums[i] != i+1) return i+1; } return n+1; }10. 性能优化技巧
- 提前终止:如果在第一遍遍历中发现1不存在,可以直接返回1
- 预处理:可以先扫描数组移除所有非正数,但要注意这会增加空间复杂度
- 位运算:如果允许少量额外空间,可以用位图来标记存在的数字
在实际面试中,建议先给出暴力解法,然后逐步优化到最优解,并清楚地解释每个优化步骤的思考过程。