LeetCode 热题 100 之 41.缺失的第一个正数

核心思路

对于长度为 n 的数组,缺失的最小正整数只可能在 [1, n+1] 范围内:

  • 如果 1n 都出现了,那么答案就是 n+1
  • 否则,答案就是其中缺失的最小正整数。

为了满足 O (n) 时间复杂度O (1) 空间复杂度 的要求,我们可以利用数组本身作为 "哈希表",将每个数 x(满足 1 ≤ x ≤ n)放到它对应的索引位置 x-1 处。最后遍历数组,第一个不满足 nums[i] == i+1 的位置 i+1 就是答案。


解题步骤

  1. 原地置换 :遍历数组,对于每个元素 nums[i],如果它是 1n 之间的数,并且不在正确的位置(nums[i] != nums[nums[i]-1]),就将它交换到 nums[i]-1 的位置。
  2. 查找缺失 :再次遍历数组,找到第一个位置 i,使得 nums[i] != i+1,返回 i+1。如果所有位置都满足,返回 n+1

Java 实现

复制代码
public int firstMissingPositive(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        // 将 1~n 的数放到对应的位置
        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;
        }
    }
    // 1~n 都存在,返回 n+1
    return n + 1;
}

示例验证

示例 1nums = [1,2,0]

  • 遍历后数组为 [1,2,0]
  • 检查:nums[0]=1nums[1]=2nums[2]=0 != 3,返回 3

示例 2nums = [3,4,-1,1]

  • 遍历后数组为 [1,-1,3,4]
  • 检查:nums[0]=1nums[1]=-1 != 2,返回 2

示例 3nums = [7,8,9,11,12]

  • 遍历后数组不变,所有元素都不在 1~5 范围内。
  • 检查:nums[0]=7 != 1,返回 1

复杂度分析

  • 时间复杂度:O (n),每个元素最多被交换到正确的位置一次,两次线性遍历。
  • 空间复杂度:O (1),仅使用常数额外空间,原地修改数组。

扩展:官方题解

官方题解的思路是:

利用数组本身作为 "标记容器",通过正负号标记「1~n 范围内的数是否出现过」------ 正数表示未出现,负数表示已出现,最终第一个正数对应的索引 + 1 就是缺失的最小正整数。

逐行代码解释

复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        // 步骤1:获取数组长度n(核心:缺失的最小正整数只在[1, n+1]范围内)
        int n = nums.length;

        // 步骤2:将所有非正整数(≤0)替换为n+1(这些数对结果无影响,统一标记为"无效值")
        for (int i = 0; i < n; ++i) {
            if (nums[i] <= 0) {
                nums[i] = n + 1;
            }
        }

        // 步骤3:用"正负号"标记1~n范围内的数是否出现过
        for (int i = 0; i < n; ++i) {
            // 取绝对值:避免之前的标记(负号)影响判断
            int num = Math.abs(nums[i]);
            // 仅处理1~n范围内的数(超出的数无意义)
            if (num <= n) {
                // 将num对应的索引(num-1)位置的数标记为负数,表示num已出现
                // 再次取绝对值:避免重复标记导致变正(比如数组有重复数时)
                nums[num - 1] = -Math.abs(nums[num - 1]);
            }
        }

        // 步骤4:遍历数组,找到第一个正数的索引,返回索引+1(即为缺失的最小正整数)
        for (int i = 0; i < n; ++i) {
            if (nums[i] > 0) {
                return i + 1;
            }
        }

        // 步骤5:如果数组全为负数(1~n都出现了),返回n+1
        return n + 1;
    }
}

关键步骤深度拆解

步骤 2:替换非正整数为 n+1
  • 为什么要替换?:非正整数(0、负数)不是我们要找的 "正整数",留着会干扰后续的标记逻辑(比如负数会被误判为 "已标记");
  • 为什么替换成 n+1?:n+1 是 "超出有效范围的数"(我们只关心 1~n),后续处理时会被过滤掉,不会影响标记。
步骤 3:核心标记逻辑(最关键)
  • 核心公式 :数num对应数组索引num-1(比如数 1 对应索引 0,数 3 对应索引 2);
  • 取绝对值的原因
    1. 第一步替换后,nums[i]可能是正数(原正数 / 替换的 n+1),也可能是后续被标记的负数;
    2. 必须用Math.abs(nums[i])拿到原始数值,才能判断它是否在 1~n 范围内;
  • -Math.abs(nums[num-1])的原因 :避免重复标记导致符号反转(比如数组有重复数[2,2]):
    • 第一次处理num=2nums[1]变为负数;
    • 第二次处理num=2:如果直接写nums[1] = -nums[1],会把负数变回正数,导致标记失效;
    • -Math.abs(...)能保证无论原值是正 / 负,最终都变为负数,标记不失效。

完整示例验证(nums = [3,4,-1,1],n=4)

表格

步骤 操作 数组状态 说明
初始 - [3,4,-1,1] 原始数组
步骤 2 替换≤0 的数为 5(n+1=5) [3,4,5,1] -1 被替换为 5
步骤 3(i=0) num=3(≤4)→ nums[2] = -5 [3,4,-5,1] 标记数 3 已出现
步骤 3(i=1) num=4(≤4)→ nums[3] = -1 [3,4,-5,-1] 标记数 4 已出现
步骤 3(i=2) num=5(>4)→ 不处理 [3,4,-5,-1] 5 超出范围,跳过
步骤 3(i=3) num=1(≤4)→ nums[0] = -3 [-3,4,-5,-1] 标记数 1 已出现
步骤 4 遍历找第一个正数 索引 1 的数是 4(正数) 返回 1+1=2

最终结果为 2,和预期一致。

另一示例验证(nums = [1,2,0],n=3)

表格

步骤 操作 数组状态 说明
初始 - [1,2,0] 原始数组
步骤 2 替换 0 为 4 [1,2,4] 0 被替换为 4
步骤 3(i=0) num=1→nums[0]=-1 [-1,2,4] 标记 1 已出现
步骤 3(i=1) num=2→nums[1]=-2 [-1,-2,4] 标记 2 已出现
步骤 3(i=2) num=4>3→不处理 [-1,-2,4] 4 超出范围
步骤 4 遍历找第一个正数 索引 2 的数是 4(正数) 返回 2+1=3

最终结果为 3,符合预期。

总结(关键点回顾)

  1. 核心思想:用数组索引映射 1~n 的正整数,通过正负号标记 "数是否出现",无需额外空间;
  2. 关键处理
    • 替换非正整数为 n+1,避免干扰标记;
    • 全程用绝对值处理,避免重复标记 / 负号干扰;
  3. 结果判断:第一个正数的索引 + 1 是答案,全负数则返回 n+1;
  4. 复杂度:时间 O (n)(三次线性遍历),空间 O (1)(原地修改),完美满足题目约束。

这种 "标记法" 是原地哈希的经典应用,和之前的 "置换法" 相比,逻辑更简洁,且避免了交换操作的边界问题,是这道题的最优解法之一。

相关推荐
码上发达2 小时前
状态压缩搜索解法(DFS + Dominance)
算法
颜酱2 小时前
差分数组:高效处理数组区间批量更新的核心技巧
javascript·后端·算法
yyy(十一月限定版)2 小时前
图论——最小生成树Kruskal算法
算法·图论
宇木灵2 小时前
C语言基础-十一、递归与分治(完结)
c语言·开发语言·学习·算法
We་ct3 小时前
LeetCode 173. 二叉搜索树迭代器:BSTIterator类 实现与解析
前端·算法·leetcode·typescript
weixin_395448913 小时前
main.c_0222cursor
c语言·前端·算法
Zik----3 小时前
Leetcode27 —— 移除元素(双指针)
数据结构·算法
踩坑记录3 小时前
leetcode hot100 79. 单词搜索 medium 递归回溯
leetcode
陆嵩3 小时前
GMRES 方法的数学推导及其算法表示
算法·概率论·arnoldi·gmres·minres·givens·hessenberg