当前位置: 首页 > news >正文

【LeetCode Hot 100】 - 缺失的第一个正数完全题解

题目描述

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

请你实现时间复杂度为O(n)并且只使用常数级别额外空间的解决方案。

示例 1:

text

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

示例 2:

text

输入:nums = [3,4,-1,1] 输出:2 解释:1 在数组中,2 缺失

示例 3:

text

输入:nums = [7,8,9,11,12] 输出:1 解释:1 不在数组中

示例 4:

text

输入:nums = [1] 输出:2

示例 5:

text

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

解题思路

核心思想:对于一个长度为n的数组,缺失的第一个正数一定在[1, n+1]范围内。

证明:

  • 如果数组包含了1n的所有正整数,那么答案就是n+1

  • 否则,答案就是[1, n]中缺失的那个数

因此,我们只需要关注1n范围内的数字,将它们放到正确的位置上。


方法一:哈希表(不满足空间要求)

思路

将所有数字存入哈希表,然后从 1 开始递增查找。

代码实现

java

public int firstMissingPositive(int[] nums) { Set<Integer> set = new HashSet<>(); for (int num : nums) { set.add(num); } int missing = 1; while (set.contains(missing)) { missing++; } return missing; }

复杂度分析

  • 时间复杂度:O(n) — 一次遍历 + 最多 n 次查找

  • 空间复杂度:O(n) — 哈希表存储所有元素

优缺点

  • ✅ 思路简单直观

  • ❌ 空间复杂度 O(n),不满足题目常数空间要求


方法二:排序(不满足时间要求)

思路

排序后跳过负数和零,然后找第一个缺失的正数。

代码实现

java

public int firstMissingPositive(int[] nums) { Arrays.sort(nums); int missing = 1; for (int num : nums) { if (num == missing) { missing++; } else if (num > missing) { break; } } return missing; }

复杂度分析

  • 时间复杂度:O(n log n) — 排序

  • 空间复杂度:O(log n) — 排序所需空间(或 O(1) 如果原地排序)

优缺点

  • ✅ 代码简单

  • ❌ 时间复杂度 O(n log n),不满足题目 O(n) 要求


方法三:原地哈希(最优解)⭐

核心思想

利用数组索引作为哈希表,将数字x放到索引x-1的位置上。

规则:

  • 只处理1n范围内的数字

  • nums[i]放到nums[nums[i] - 1]的位置

  • 最终,如果nums[i] != i + 1,则i+1就是缺失的第一个正数

算法步骤

  1. 遍历数组,对于每个元素nums[i]

    • 如果1 <= nums[i] <= nnums[i]不在正确位置上,就交换

    • 交换后继续检查新换过来的数字

  2. 再次遍历,找到第一个nums[i] != i + 1的位置

  3. 如果都正确,返回n + 1

代码实现

java

public int firstMissingPositive(int[] nums) { int n = nums.length; // 第一步:将数字放到正确的位置 for (int i = 0; i < n; i++) { // 循环交换,直到 nums[i] 不在 [1, n] 范围内或者已经在正确位置 while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) { swap(nums, i, nums[i] - 1); } } // 第二步:找到第一个位置不对的数字 for (int i = 0; i < n; i++) { if (nums[i] != i + 1) { return i + 1; } } // 第三步:如果都在正确位置,返回 n+1 return n + 1; } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }

手动模拟

nums = [3, 4, -1, 1]为例(n=4):

第一步:原地交换

inums 数组操作
0[3,4,-1,1]nums[0]=3,范围1-4,目标位置2,交换 nums[0] 和 nums[2] → [ -1,4,3,1 ]
0[-1,4,3,1]nums[0]=-1,不在范围,跳过
1[-1,4,3,1]nums[1]=4,目标位置3,交换 nums[1] 和 nums[3] → [ -1,1,3,4 ]
1[-1,1,3,4]nums[1]=1,目标位置0,交换 nums[1] 和 nums[0] → [ 1,-1,3,4 ]
1[1,-1,3,4]nums[1]=-1,不在范围,跳过
2[1,-1,3,4]nums[2]=3,目标位置2,已经在正确位置
3[1,-1,3,4]nums[3]=4,目标位置3,已经在正确位置

最终数组:[1, -1, 3, 4]

第二步:查找缺失正数

索引 i期望值 i+1实际值 nums[i]是否匹配
011
12-1❌ 返回 2

结果:2


另一种写法(标记法)

思路

  1. 先将所有负数和零改为n+1(占位符)

  2. 遍历数组,将|nums[i]|对应位置的数字标记为负数

  3. 遍历数组,第一个正数的位置就是答案

代码实现

java

public int firstMissingPositive(int[] nums) { int n = nums.length; // 第一步:将所有负数、0 改为 n+1(这些数字不影响结果) for (int i = 0; i < n; i++) { if (nums[i] <= 0) { nums[i] = n + 1; } } // 第二步:使用索引作为哈希表,标记出现过的正数 for (int i = 0; i < n; i++) { int num = Math.abs(nums[i]); if (num <= n) { // 将 num-1 位置的数字标记为负数 if (nums[num - 1] > 0) { nums[num - 1] = -nums[num - 1]; } } } // 第三步:找到第一个正数的位置 for (int i = 0; i < n; i++) { if (nums[i] > 0) { return i + 1; } } // 如果全部被标记,返回 n+1 return n + 1; }

手动模拟(标记法)

nums = [3, 4, -1, 1](n=4)

第一步:处理负数和零

text

[3, 4, -1, 1] → [-1 ≤ 0? 改为5] → [3, 4, 5, 1]

第二步:标记出现过的数字

  • num=3,标记位置2:[3, 4, 5, 1][3, 4, -5, 1]

  • num=4,标记位置3:[3, 4, -5, 1][3, 4, -5, -1]

  • num=5,跳过(5 > 4)

  • num=1,标记位置0:[3, 4, -5, -1][-3, 4, -5, -1]

第三步:找第一个正数

  • i=0,nums[0] = -3 < 0

  • i=1,nums[1] = 4 > 0 → 返回 i+1 = 2 ✅


复杂度分析(两种写法)

  • 时间复杂度:O(n) — 最多三次线性扫描

  • 空间复杂度:O(1) — 原地修改数组

优缺点

  • ✅ 时间复杂度 O(n)

  • ✅ 空间复杂度 O(1)

  • ✅ 满足题目要求

  • ⭐ 面试推荐写法


方法四:置换法(带边界优化)

思路

与方法三相同,但增加了条件判断,避免无效交换。

代码实现

java

public int firstMissingPositive(int[] nums) { int n = nums.length; for (int i = 0; i < n; i++) { // 只有当 nums[i] 在 [1, n] 范围内,且不在正确位置时才交换 while (nums[i] >= 1 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) { int targetIndex = nums[i] - 1; // 交换 int temp = nums[i]; nums[i] = nums[targetIndex]; nums[targetIndex] = temp; } } for (int i = 0; i < n; i++) { if (nums[i] != i + 1) { return i + 1; } } return n + 1; }

方法对比总结

方法时间复杂度空间复杂度是否满足题目要求推荐度
哈希表O(n)O(n)❌ 空间不满足⭐⭐
排序O(n log n)O(log n)❌ 时间不满足⭐⭐
原地哈希(交换法)O(n)O(1)✅ 完美满足⭐⭐⭐⭐⭐
原地哈希(标记法)O(n)O(1)✅ 完美满足⭐⭐⭐⭐

图文详解(以交换法为例)

核心思想图解

text

目标:将数字放到正确的位置 数字 x 应该放在索引 x-1 的位置 数组索引: 0 1 2 3 4 5 正确数字: 1 2 3 4 5 6

交换过程可视化

text

初始数组: [3, 4, -1, 1] (n=4) 步骤1: i=0, nums[0]=3 3 应该去索引 2 交换 nums[0] 和 nums[2] [ -1, 4, 3, 1 ] 步骤2: i=0, nums[0]=-1 (不在1-4范围,跳过) i=1, nums[1]=4 4 应该去索引 3 交换 nums[1] 和 nums[3] [ -1, 1, 3, 4 ] 步骤3: i=1, nums[1]=1 1 应该去索引 0 交换 nums[1] 和 nums[0] [ 1, -1, 3, 4 ] 步骤4: i=1, nums[1]=-1 (跳过) i=2, nums[2]=3 (已在正确位置) i=3, nums[3]=4 (已在正确位置) 最终数组: [1, -1, 3, 4] 检查: 索引0: 期望1 → 实际1 ✅ 索引1: 期望2 → 实际-1 ❌ 返回2

为什么时间复杂度是 O(n)?

虽然使用了while循环,但每个数字最多被交换一次,一旦放到正确位置就不会再移动。总交换次数 ≤ n,因此整体是 O(n)。


常见问题 Q&A

Q1:为什么要限制范围在 1 到 n?

A:因为答案只可能在这个范围内。如果[1, n]都出现了,答案就是n+1。超过n的数字我们可以忽略。

Q2:交换法中的 while 循环会不会导致死循环?

A:不会。每次交换都会把一个数字放到正确位置,正确位置的数字不会再被移动。最多交换 n 次后循环结束。

Q3:标记法中为什么要用绝对值?

A:因为标记过程中可能把某个位置变成了负数,取绝对值可以获取原始数值。

Q4:两种原地哈希方法哪个更好?

A:

  • 交换法:更直观,更容易理解,推荐面试使用

  • 标记法:需要两次遍历,代码稍复杂,但思想也很巧妙

Q5:如何处理数组中已有的重复数字?

A:交换时会自动处理。当遇到重复时,nums[nums[i] - 1] != nums[i]条件为 false,不会交换,避免了死循环。


边界情况处理

输入输出说明
[]1空数组,第一个正数是1
[0]1只有0,1缺失
[-1, -2, -3]1全是负数,1缺失
[1]2有1,缺失2
[1, 2, 3, 4, 5]61-5都在,返回6
[1, 1, 1, 1]2重复数字,缺失2

易错点总结

  1. 交换条件写错:必须是nums[nums[i] - 1] != nums[i],否则会无限交换

  2. 忘记处理重复数字:重复数字会导致死循环

  3. 索引越界:交换前必须检查nums[i][1, n]范围内

  4. 使用 while 而非 if:因为交换后新数字可能还需要处理

  5. 标记法忘记取绝对值:标记后数字变负数,需要取绝对值判断


总结

缺失的第一个正数是 LeetCode Hot 100 中一道考察原地算法数组索引映射的经典题目。

面试建议:

  1. 先分析答案范围在[1, n+1]

  2. 给出哈希表解法(虽然不满足空间要求,但展示思路)

  3. 最后给出原地哈希的交换法(展示深度理解)

  4. 解释为什么时间复杂度是 O(n)

核心要点:

  • 理解答案范围限制[1, n+1]

  • 掌握利用数组索引作为哈希表的技巧

  • 理解原地交换的原理

  • 能够处理边界情况(负数、零、重复、空数组)

关键公式:

  • 数字x的正确位置:x-1

  • 位置i的正确数字:i+1


参考链接

  • LeetCode 41. 缺失的第一个正数 题目链接

  • LeetCode 41. 缺失的第一个正数 官方题解

http://www.cnnetsun.cn/news/1932125.html

相关文章:

  • 终章|黑天鹅频发?ToB老板的底牌是做一艘“打不沉的船”
  • IO复用:epoll
  • Jasminum插件:中文文献管理的终极解决方案
  • PyTorch 2.8镜像代码实例:基于Accelerate的跨GPU模型并行训练脚本
  • Step3-VL-10B开源大模型:模型权重分片加载与内存峰值控制
  • 幻境·流金多场景落地:游戏原画概念图快速迭代与美术风格统一控制
  • 去掉 UWB / 蓝牙标签:浙江普陀时空大数据应用技术联合研究院无感定位,免穿戴、免部署、降本、全场景覆盖
  • 2026年,还需要学Docker吗?容器运行时选型完全指南
  • 从三甲试点到基层覆盖,AI医疗咨询规模化落地的4个生死关卡,错过2026奇点大会将延迟部署周期11.8个月
  • 从HSPICE到Simscape:一个电路工程师如何用Simscape Language搞定大规模图像信号仿真
  • Java+YOLOv8+Redis实战:工业视觉检测缓存加速+实时数据同步,毫秒级生产级落地
  • USART 串口通信进阶指南:从寄存器配置到中断优化
  • AIoT产品的终极竞争:Jobs To Be Done 如何驱动从设备到服务的跃迁
  • Python3 数字(Number)
  • Linux系统与系统编程(5)——动静态库、自动化构建make/Makefile、回车与换行相关的行缓冲区
  • 【生成式AI成本控制黄金法则】:20年架构师亲授5大降本增效实战策略
  • 讯飞虚拟人简单交互实现
  • 避坑指南:在Unity中正确配置PICO 4 Ultra的外部文件读写权限(含AndroidManifest.xml模板)
  • LeetCode 232. Implement Queue using Stacks 题解
  • 三菱FX3U与多台力士乐VFC-x610变频器通讯实现案例详解:频率设定、启停及读取功能附程序...
  • 最小二乘法的5种变体:从加权到自适应,选对方法让模型精度翻倍
  • 科研党必备:scihub-cn命令行工具5分钟搞定文献批量下载(附常见报错解决方案)
  • 三大权威MEG公开数据集深度解析与应用指南
  • 从卫星原始数据到地表真实信息:一份给Python+GDAL用户的辐射校正全流程脚本指南
  • 缓存命中率暴跌?生成式AI应用缓存策略全解析,从Token级语义缓存到动态TTL自适应调优
  • 开关电源功率因数与效率测试实战指南
  • 爆肝整理,智能体发展万字综述!!!
  • c++学习笔记——堆
  • 没做这4类边界测试就部署AI服务?:金融/医疗/客服场景下 hallucination 爆发的3小时应急响应流程
  • AI服务注册延迟超800ms?2024年最严苛生产环境实测:3种服务发现方案TPS对比(含Latency P999数据)