☰
寻找缺失最小正整数的O(1)空间解法
2026/10/31 15:36:35 网站建设 项目流程

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)空间,我们需要利用数组本身作为哈希表。具体思路是:

  1. 遍历数组,将每个正整数x放到它应该在的位置(即索引x-1处)
  2. 再次遍历数组,第一个位置不正确的索引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+1

3.3 关键点解析

  • while循环中的交换操作确保每个数字最终都在正确位置
  • 条件1 <= nums[i] <= n确保我们只处理可能的正整数
  • 条件nums[nums[i]-1] != nums[i]避免无限循环

注意:这个解法会修改原数组,如果题目要求不能修改输入,则需要使用其他方法。

4. 边界情况与测试用例

4.1 常见边界情况

  1. 空数组:应返回1
  2. 全负数数组:应返回1
  3. 连续正整数数组:如[1,2,3]应返回4
  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()) == expected

5. 算法复杂度分析

5.1 时间复杂度

虽然代码中有嵌套循环,但每个数字最多被交换到正确位置一次,因此总时间复杂度是O(n)。

5.2 空间复杂度

除了几个临时变量外,没有使用额外空间,因此空间复杂度是O(1)。

6. 实际应用场景

这个问题虽然抽象,但其解法思想在实际中有重要应用:

  1. 数据库系统中的缺失ID检测
  2. 连续编号系统中的空缺位置查找
  3. 内存管理中的空闲块查找
  4. 分布式系统中的序列号完整性检查

7. 变种问题与扩展思考

7.1 变种问题

  1. 找出所有缺失的正整数
  2. 找出第一个缺失的非负整数(包含0)
  3. 流式数据中的缺失正整数查找(无法存储整个数组)

7.2 扩展思考

  • 如果允许O(n)空间但要求单次遍历,如何解决?
  • 如果数组已经排序,是否有更优解法?
  • 如何将这个算法并行化处理超大数组?

8. 编码实现中的常见陷阱

  1. 索引越界:在处理nums[nums[i]-1]时要确保索引有效
  2. 无限循环:交换时需要检查是否已经处于正确位置
  3. 重复数字:需要正确处理重复数字的情况
  4. 负数处理:要跳过负数和零,但不要误判

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不存在,可以直接返回1
  2. 预处理:可以先扫描数组移除所有非正数,但要注意这会增加空间复杂度
  3. 位运算:如果允许少量额外空间,可以用位图来标记存在的数字

在实际面试中,建议先给出暴力解法,然后逐步优化到最优解,并清楚地解释每个优化步骤的思考过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询