【力扣100题】60.缺失的第一个正数

题目描述

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

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

示例

复制代码
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。

示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。

示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。

提示:

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 备注
原地哈希 将每个数放到它值对应的位置 O(n) O(1) 推荐解法
哈希表 用额外空间记录出现过的数 O(n) O(n) 简单但空间不优
排序后遍历 先排序再找缺失 O(n log n) O(1) 时间不优

一、核心解法:原地哈希(桶排序思想)

核心思想

将数组本身作为哈希表。对于每个位置 i,我们希望 nums[i] == i + 1

  • 如果 1 <= nums[i] <= n,则它应该放在索引 nums[i] - 1 的位置
  • 通过原地交换,将每个数放到它值对应的位置
  • 最后遍历数组,第一个不满足 nums[i] == i + 1 的位置,答案就是 i + 1

关键洞察

复制代码
观察:答案一定在 [1, n+1] 范围内

原因:
- 如果 [1, n] 都在数组中,答案是 n+1
- 否则,缺失的最小正整数一定在 [1, n] 中

因此我们只需要关心 [1, n] 范围内的数,其他数(<=0 或 >n)可以直接忽略。

图解

复制代码
输入: nums = [3, 4, -1, 1]

初始状态:
  index:  0   1   2   3
  nums:  [3,  4, -1,  1]
         i=0

处理 i=0:
  nums[0] = 3
  3 > 0 && 3 <= 4 && nums[2] != 3 (nums[2] = -1)
  swap(nums[2], nums[0])
  nums = [-1, 4, 3, 1]

处理 i=0:
  nums[0] = -1
  -1 不满足 > 0,跳过

处理 i=1:
  nums[1] = 4
  4 > 0 && 4 <= 4 && nums[3] != 4 (nums[3] = 1)
  swap(nums[3], nums[1])
  nums = [-1, 1, 3, 4]

处理 i=1:
  nums[1] = 1
  1 > 0 && 1 <= 4 && nums[0] != 1 (nums[0] = -1)
  swap(nums[0], nums[1])
  nums = [1, -1, 3, 4]

处理 i=1:
  nums[1] = -1
  -1 不满足 > 0,跳过

处理 i=2, 3:
  nums[2] = 3, nums[2] == 3, 跳过
  nums[3] = 4, nums[3] == 4, 跳过

最终状态:
  index:  0   1   2   3
  nums:  [1, -1, 3, 4]
         i=1 不满足 nums[i] == i+1

遍历结果:
  i=0: nums[0]=1, i+1=1, 满足
  i=1: nums[1]=-1, i+1=2, 不满足!返回 2

二、算法流程图

复制代码
输入: nums = [3, 4, -1, 1], n = 4

第一步:原地交换(将每个数放到正确位置)

  初始化:
    nums = [3, 4, -1, 1]
    i = 0

  i=0:
    nums[0]=3, 3在[1,4]内, nums[2]=-1 != 3
    交换: [3,4,-1,1] -> [-1,4,3,1]
    nums[0]=-1, 不满足>0, 跳过

  i=1:
    nums[1]=4, 4在[1,4]内, nums[3]=1 != 4
    交换: [-1,4,3,1] -> [-1,1,3,4]
    nums[1]=1, 1在[1,4]内, nums[0]=-1 != 1
    交换: [-1,1,3,4] -> [1,-1,3,4]

  i=1:
    nums[1]=-1, 不满足>0, 跳过

  i=2:
    nums[2]=3, 3在[1,4]内, nums[2]=3 == 3, 不交换

  i=3:
    nums[3]=4, 4在[1,4]内, nums[3]=4 == 4, 不交换

第二步:遍历找缺失

  i=0: nums[0]=1, i+1=1, 满足
  i=1: nums[1]=-1, i+1=2, 不满足!

输出: 2

三、完整代码实现

cpp 复制代码
class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();

        // 第一步:原地哈希
        // 将每个在 [1, n] 范围内的数放到它值对应的位置
        for (int i = 0; i < n; i++) {
            // 注意:要用 while 而不是 if,因为交换后可能还需要继续处理
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i] - 1]
                // 把 nums[i] 放到索引为 nums[i] - 1 的位置
                swap(nums[nums[i] - 1], nums[i]);
            }
        }

        // 第二步:遍历找缺失
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        // 如果 [1, n] 都在,返回 n+1
        return n + 1;
    }
};

四、逐行解析

cpp 复制代码
for (int i = 0; i < n; i++) {
  • 遍历数组的每个位置
cpp 复制代码
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
  • nums[i] > 0:只处理正数
  • nums[i] <= n:只处理在 [1, n] 范围内的数(答案只可能在范围内)
  • nums[nums[i] - 1] != nums[i]:目标位置不是正确的值,需要交换
  • 用 while 而不是 if:因为交换后当前位置可能还是需要处理的数
cpp 复制代码
swap(nums[nums[i] - 1], nums[i]);
  • nums[i] 放到索引 nums[i] - 1 的位置
  • 例如:nums[i] = 3,则放到索引 2 的位置
cpp 复制代码
for (int i = 0; i < n; i++) {
    if (nums[i] != i + 1) {
        return i + 1;
    }
}
  • 遍历数组,第一个不满足 nums[i] == i + 1 的位置
  • 答案就是 i + 1
cpp 复制代码
return n + 1;
  • 如果所有位置都满足,返回 n + 1
  • 例如 [1,2,3] 缺失的是 4

五、为什么 while 而不是 if?

复制代码
错误示例(用 if):
  nums = [4, 2, 1]

  i=0: nums[0]=4, 交换 -> [1, 2, 4]
       此时 nums[0]=1,已经在正确位置
       但 nums[2]=4,应该检查但用 if 不会继续检查

正确示例(用 while):
  nums = [4, 2, 1]

  i=0: nums[0]=4, 交换 -> [1, 2, 4]
       此时 nums[0]=1,满足 nums[0]==1,跳出 while
       用 while 可以继续检查交换后的 nums[i]

关键点:
  while 确保当前位置的数被处理到正确为止
  if 可能会漏掉交换后的数

六、原地哈希的原理

复制代码
原地哈希本质上是桶排序的思想:

普通哈希表:
  值 v 存储在 hash[v] 的位置
  需要额外的数组

原地哈希:
  值 v 存储在索引 v-1 的位置
  直接利用原数组的空间

示例:
  如果数组中有数字 3,就把它放到 nums[2](即 3-1)的位置
  检查 nums[i] 是否等于 i+1 就能知道 i+1 是否存在

七、复杂度分析

方法 时间复杂度 空间复杂度 备注
原地哈希 O(n) O(1) 推荐
哈希表 O(n) O(n) 空间不优
排序遍历 O(n log n) O(1) 时间不优

详细分析:

复制代码
时间复杂度:
  第一步:每个元素最多被交换一次(放到正确位置后就不会再处理)
         即使 while 循环,总交换次数 <= n
  第二步:遍历 O(n)
  总计:O(n)

空间复杂度:
  只用了几个变量(n, i 等)
  没有使用额外的数组
  O(1)

八、边界情况分析

情况 处理方式
空数组 return 1
[1] 第一步不做交换,第二步 return 2
[2] 第一步不做交换(2 > n=1),第二步 return 1
[-1, -2, -3] 第一步不做交换(无正数),第二步 return 1
[1, 2, 3] 第一步排好序,第二步 return 4

示例分析

复制代码
示例1: nums = [1,2,0]
  原地哈希后: [1,2,0]
  遍历: i=2, nums[2]=0 != 3
  返回: 3

示例2: nums = [3,4,-1,1]
  原地哈希后: [1,-1,3,4]
  遍历: i=1, nums[1]=-1 != 2
  返回: 2

示例3: nums = [7,8,9,11,12]
  原地哈希后: [7,8,9,11,12] (无变化)
  遍历: i=0, nums[0]=7 != 1
  返回: 1

九、面试追问 FAQ

问题 回答要点
Q: 为什么只关心 [1, n] 范围内的数? 答案只可能在 [1, n+1] 范围内,因为如果 [1,n] 都在,答案是 n+1
Q: 为什么用 while 而不是 if? 因为交换后当前位置可能还是需要处理的数,必须继续检查
Q: nums[nums[i]-1] 会不会越界? 不会,因为条件 nums[i] <= n 保证 nums[i]-1 <= n-1,且 nums[i] > 0 保证 nums[i]-1 >= 0
Q: 能否用 set 解决? 可以,但空间复杂度 O(n),不满足要求
Q: 时间复杂度为什么是 O(n)? 每个元素最多交换一次,总交换次数 <= n
Q: 如何证明空间是 O(1)? 只用了常数个变量,没有使用与 n 相关的额外空间

十、相关题目

题目编号 题目名称 难度 核心差异
41 缺失的第一个正数 困难 原地哈希
442 数组中重复的数据 中等 原地标记
448 找到所有失踪的数字 简单 原地标记
268 缺失数字 简单 异或或求和
剑指 Offer 03 数组中重复的数字 简单 原地哈希变形

十一、总结

要点 内容
核心思想 原地哈希,将数组当作哈希表使用
关键洞察 答案在 [1, n+1] 范围内
算法 第一步:将 [1,n] 范围内的数放到正确位置
算法 第二步:遍历找第一个不满足 nums[i] == i+1 的位置
时间复杂度 O(n)(每个元素最多交换一次)
空间复杂度 O(1)(原地操作)
关键点 用 while 而非 if,确保数被处理到正确位置
易错点 忽略 nums[nums[i]-1] 的边界检查

缺失的第一个正数是经典的原地哈希问题,核心思想是将数组本身作为哈希表,利用索引来表示数是否出现过。难点在于理解原地交换的逻辑和为什么用 while 而不是 if。


相关推荐
Han.miracle2 小时前
Java HashMap 与 ConcurrentHashMap 核心原理总结:从 Hash 冲突到 LongAdder
java·算法·哈希算法
菜菜的顾清寒2 小时前
力扣HOT100(35)回溯-全排列
算法·leetcode·职场和发展
Tisfy2 小时前
LeetCode 3121.统计特殊字母的数量 II:状态机
算法·leetcode·题解·状态机
洛水水2 小时前
【力扣100题】61.和为 K 的子数组
算法·leetcode·哈希算法
sheeta19982 小时前
LeetCode 补拙笔记 日期:2026.05.27 题目:61. 旋转链表
笔记·leetcode·链表
过期动态16 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq
菜菜的顾清寒18 小时前
力扣HOT100(32)二叉树的中序遍历
数据结构·算法·leetcode
csdn_aspnet19 小时前
java 算法 LeetCode 编号 70 - 爬楼梯
java·开发语言·算法·leetcode
_日拱一卒20 小时前
LeetCode:200岛屿数量
算法·leetcode·职场和发展