41. 缺失的第一个正数 (Hard)

专题归类: 数组 · 哈希表 LeetCode 链接: https://leetcode.cn/problems/first-missing-positive/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。

要求: 时间复杂度 O(n) 且空间复杂度为 O(1)(原地操作)。

示例 1:

输入:nums = [1,2,0]
输出:3
解释:1 和 2 都出现了,缺失的最小正数是 3

示例 2:

输入:nums = [3,4,-1,1]
输出:2

示例 3:

输入:nums = [7,8,9,11,12]
输出:1

补充说明:

  • 1 <= nums.length <= 5 * 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 时间复杂度 O(n),空间复杂度 O(1)

题目详细分析

数据范围分析:

  • 数组长度最大 5×10^5,O(n) 时间可行,O(n log n) 的排序勉强可行但排序后无法保持 O(1) 空间(除非用堆排序)。
  • 元素值范围很大(32 位有符号整数),无法用布尔数组标记(空间会太大)。
  • 空间 O(1) 意味着不能使用哈希表、集合或布尔数组。

输入输出特征:

  • 输入是未排序的整数数组(含负数、零、正数),输出是最小的缺失正整数。
  • 答案一定在 [1, n+1] 范围内(n 为数组长度)——因为如果有 n 个数,最小缺失正数最大就是 n+1(当 1~n 都存在时)。

边界条件:

  • 数组全部为负数或零 → 最小缺失正数是 1。
  • 数组包含 1~n 的所有数 → 答案是 n+1(如 [1,2,3] → 4)。
  • 数组包含重复数 → 不影响结果,重复元素就地处理时要注意死循环。
  • 数组只有一个元素 → 如果该元素是 1 则答案为 2,否则答案为 1。

隐藏条件:

  • “缺失的最小正数”这个定义决定了答案范围是 [1, n+1],这正是原地哈希的核心依据。
  • O(1) 空间限制了只能用数组本身做哈希表,即把值 x 放到索引 x-1 的位置。
  • 不需要考虑负数和大于 n 的数,因为它们无法占据 1~n 的位置。

小白版直白理解

想象你们班有 n 个学生,学号从 1 开始编号。老师让你检查:最小的没被使用的学号是多少?

但学号单是乱的,而且有些学号可能不对(负数、0、或者特别大的数字)。

朴素的方法: 拿个本子从 1 开始记,看 1 有没有出现,2 有没有出现……这就是用额外空间。

空间 O(1) 的方法: 把学号单看作 n 个座位,规定”学号为 x 的同学应该坐在第 x 个座位上”(就是索引 x-1)。

  1. 遍历座位,如果座位上的人坐错了(比如学号 3 坐在了第 1 个座位上),就让他去正确的座位
  2. 交换后,新来这个座位的人也可能坐错了,继续让他归位
  3. 全部归位后,再扫描一遍:第一个座位坐的不是学号 1 → 最小缺失就是 1;第二个座位坐的不是学号 2 → 最小缺失就是 2……

为什么范围是 1 到 n+1? 因为总共只有 n 个位置,最小缺失正数不可能超过 n+1。比如 3 个座位,如果 1、2、3 都有人了,那缺失的就是 4。


解题思路

思路一:原地哈希(推荐)

核心洞察:

答案一定在 [1, n+1] 范围内。我们把数组当作哈希表:

  • 值 1 应该放在索引 0
  • 值 2 应该放在索引 1
  • 值 x(在 [1, n] 内)应该放在索引 x-1

通过交换操作将每个数归位,然后扫描找到第一个位置不对的,就是缺失的最小正数。

def firstMissingPositive(nums):
    n = len(nums)
    
    # Step 1: 原地归位
    for i in range(n):
        # while 循环:不断交换直到当前位置的值归位或无法归位
        while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
            # 将 nums[i] 放到它应该在的位置
            correct_pos = nums[i] - 1
            nums[i], nums[correct_pos] = nums[correct_pos], nums[i]
    
    # Step 2: 扫描找缺失值
    for i in range(n):
        if nums[i] != i + 1:
            return i + 1
    
    # Step 3: 全部归位,说明 1~n 都存在
    return n + 1

为什么用 while 而不是 if: 交换后,当前位置来了一个新值,这个新值可能也需要归位。while 确保当前位置的值最终要么归位要么不需要归位(负数、0、或 > n)。

复杂度: O(n) 时间(每个元素最多被交换两次),O(1) 空间。

思路二:标记法(另一种原地哈希)

先处理数组中的数,把不在 [1, n] 范围内的数统一替换为一个特殊值(如 n+1)。然后遍历数组,对每个值 x,标记索引 x-1 为负数(表示 x 存在)。最后扫描找到第一个正数索引。

def firstMissingPositive_mark(nums):
    n = len(nums)
    
    # Step 1: 将非正数和大于 n 的数替换为 n+1
    for i in range(n):
        if nums[i] <= 0 or nums[i] > n:
            nums[i] = n + 1
    
    # Step 2: 用负号标记存在的数
    for i in range(n):
        val = abs(nums[i])
        if val <= n:
            # 将索引 val-1 标记为负数(表示 val 存在)
            if nums[val - 1] > 0:
                nums[val - 1] = -nums[val - 1]
    
    # Step 3: 找到第一个正数的索引
    for i in range(n):
        if nums[i] > 0:
            return i + 1
    
    return n + 1

思路三:排序法(不满足要求,但易于理解)

排序后扫描,用 expected = 1 来依次检查。O(n log n) 时间,不满足 O(n) 要求,且排序一般需要 O(n) 额外空间。

def firstMissingPositive_sort(nums):
    nums.sort()
    expected = 1
    for num in nums:
        if num == expected:
            expected += 1
        elif num > expected:
            # 出现大于 expected 的数,说明 expected 缺失
            return expected
    return expected

易错点

  • 使用 while 而非 if:在交换过程中,当前索引位置获得了新值,必须继续处理直到该位置的值归位或不需要归位。用 if 会导致遗漏。
  • 交换顺序nums[i], nums[nums[i]-1] 这个交换顺序有问题!因为 Python 中右侧的 nums[i] 会先被计算还是先被赋值?正确写法是用临时变量或先存正确位置:correct_pos = nums[i] - 1; nums[i], nums[correct_pos] = nums[correct_pos], nums[i]
  • 死循环处理nums[nums[i] - 1] != nums[i] 这个条件防止了死循环——如果目标位置已经有相同的值,说明重复,不需要交换。
  • 值范围判断:只处理 1 <= nums[i] <= n 的值,负数、0、大于 n 的值不参与交换。
  • 索引从 0 开始:值 x 对应索引 x-1,不是 x。这是最常见的 off-by-one 错误。
  • 答案范围:如果 1~n 都存在,答案是 n+1,不是 n。

框架提炼

原地哈希通用模板(值映射到索引):

def firstMissingPositive(nums):
    n = len(nums)
    
    # Step 1: 将所有在范围内的值放到正确的位置
    # 核心逻辑:值 x (1 <= x <= n) 应该放在索引 x-1
    for i in range(n):
        while 条件判断(当前位置的值需要归位):
            交换(当前位置, 目标位置)
    
    # Step 2: 扫描找到第一个不匹配的位置
    for i in range(n):
        if nums[i] != i + 1:
            return i + 1  # 第一个缺失的正数
    
    # Step 3: 全部匹配
    return n + 1

条件判断通用形式:

while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:

原地哈希的适用特征:

  1. 问题涉及一组值在确定范围内(通常是 [1, n] 或 [0, n-1])
  2. 空间复杂度要求 O(1)
  3. 可以用数组本身代替哈希表

同类原地哈希问题:


关联题目

  • 287-寻找重复数 — 同样是值范围 [1, n] 的原地哈希问题,但找的是重复的数而不是缺失的数。可以用原地哈希或链表判圈法。
  • 448-找到所有数组中消失的数字 — 和本题高度相似,标记法可以直接应用。区别在于本题求最小缺失,448 求所有缺失。
  • 442-数组中重复的数据 — 也是值范围 [1, n]、用原地标记法找出重复元素。
  • 268-丢失的数字 — 更简单版本:值范围 [0, n],找缺失的一个数,可以用求和公式或异或解。