hot 100 第十七题 17.缺失的第一个正数

题目:

给你一个未排序的整数数组 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 没有出现。

核心思路

利用数组本身作为哈希表:将每个正数放到它应该在的位置上。

关键观察:

  • 长度为 n 的数组,缺失的第一个正数一定在 [1, n+1] 范围内

  • 如果 1~n 都存在,答案就是 n+1

  • 如果缺了某个数,答案就是最小的缺失数

    nums = [3, 4, -1, 1]
    长度 n=4,答案一定在 [1, 2, 3, 4, 5] 中

    理想状态:把数字放到对应位置
    index: 0 1 2 3
    nums: [1, 2, 3, 4] → 如果能做到这样,第一个不匹配的就是答案

题解

java 复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        
        // 第一步:将每个数放到正确的位置
        // 数字 i 应该放在 nums[i-1] 的位置
        for (int i = 0; i < n; i++) {
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i] - 1]
                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;
    }
}
```

## 详细演示
```
nums = [3, 4, -1, 1]

第一步:将数字放到正确位置
目标:数字 1 放在 nums[0],数字 2 放在 nums[1],以此类推

i=0: nums[0]=3
     3 应该在 nums[2],交换 nums[0] 和 nums[2]
     [-1, 4, 3, 1]
     
     继续处理 nums[0]=-1
     -1 不是正数或超出范围,跳过

i=1: nums[1]=4
     4 应该在 nums[3],交换 nums[1] 和 nums[3]
     [-1, 1, 3, 4]
     
     继续处理 nums[1]=1
     1 应该在 nums[0],交换 nums[1] 和 nums[0]
     [1, -1, 3, 4]
     
     继续处理 nums[1]=-1
     -1 不是正数,跳过

i=2: nums[2]=3
     3 应该在 nums[2],已经在正确位置 ✓

i=3: nums[3]=4
     4 应该在 nums[3],已经在正确位置 ✓

整理后: [1, -1, 3, 4]
         ↑   ↑  ↑  ↑
        位置0 1  2  3

第二步:找第一个不匹配
i=0: nums[0]=1, 期望=1 ✓
i=1: nums[1]=-1, 期望=2 ✗ 找到了!

答案: 2

while 循环的条件

java 复制代码
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i])
```

三个条件:
1. `nums[i] > 0` --- 只处理正数
2. `nums[i] <= n` --- 只处理 [1, n] 范围内的数(超出范围的无意义)
3. `nums[nums[i] - 1] != nums[i]` --- 目标位置的值不等于当前值(避免死循环)
```
例子:nums = [1, 1]
i=0: nums[0]=1 应该在 nums[0],已经在正确位置
     如果没有第三个条件,会一直交换自己,死循环
```

### 为什么用 while 不用 if?

因为交换后,当前位置的新值可能还需要继续交换:
```
nums = [3, 4, -1, 1]

i=1: nums[1]=4
     交换后 nums = [3, 1, -1, 4]
     现在 nums[1]=1,还需要继续交换到 nums[0]
     所以用 while 循环持续处理
```

## 图解过程
```
nums = [3, 4, -1, 1], n=4

目标映射:
数字 1 → index 0
数字 2 → index 1
数字 3 → index 2
数字 4 → index 3

初始:  [3, 4, -1, 1]
        ↓     ↓    ↓
       应该在2   应该在3  应该在0

交换过程:
[3, 4, -1, 1]
 ↓        ↓
交换 3 和 -1
[-1, 4, 3, 1]

继续处理 index 1:
[-1, 4, 3, 1]
     ↓     ↓
交换 4 和 1
[-1, 1, 3, 4]

继续处理 index 1:
[-1, 1, 3, 4]
  ↓  ↓
交换 -1 和 1
[1, -1, 3, 4]

最终状态:
index: 0   1   2   3
nums: [1, -1,  3,  4]
期望: [1,  2,  3,  4]
           ↑
        第一个缺失的是 2
```

## 另一个例子
```
nums = [7, 8, 9, 11, 12]

第一步:整理
所有数字都 > n=5,都不在 [1, 5] 范围内
整理后数组不变: [7, 8, 9, 11, 12]

第二步:检查
i=0: nums[0]=7, 期望=1 ✗

答案: 1

边界情况

java 复制代码
// 1. 数组只包含1
nums = [1]
答案: 2

// 2. 数组连续 1~n
nums = [1, 2, 3]
答案: 4 (n+1)

// 3. 包含重复
nums = [1, 1]
整理后: [1, 1]
答案: 2

// 4. 都是负数或0
nums = [-1, -2, 0]
整理后: [-1, -2, 0] (都不动)
答案: 1

复杂度分析

  • 时间复杂度 : O(n)
    • 虽然有 while 循环嵌套 for,但每个数最多交换一次到正确位置
    • 每个数最多被访问常数次
  • 空间复杂度 : O(1)
    • 只用了原地交换,没有额外空间

为什么这个方法有效?

本质是原地哈希

  • 用数组下标作为"哈希桶"
  • 数字 存放在 位置i``nums[i-1]
  • 超出范围的数字(负数、0、>n)不管,它们不影响答案
  • 最后扫描一遍,第一个 的位置就是答案nums[i] != i+1

这是一种非常巧妙的技巧,将额外空间的哈希表优化成了原地操作。

本质

这道题结合了多个技巧:

  1. 原地哈希 --- 用数组本身存储信息
  2. 范围限定 --- 答案一定在 [1, n+1]
  3. 交换归位 --- 将元素放到它应该在的位置
相关推荐
艾iYYY6 分钟前
类和对象(详解初始化列表, static成员变量, 友元,内部类)
c语言·数据结构·c++·算法
AbandonForce12 分钟前
C++11:列表初始化||右值和移动语义||引用折叠和完美转发||可变参数模板||lambda表达式||包装器(function bind)
开发语言·数据结构·c++·算法
khalil102017 分钟前
代码随想录算法训练营Day-50 图论02 | 99.岛屿数量-深搜、99.岛屿数量-广搜 、100.岛屿的最大面积
数据结构·c++·算法·leetcode·深度优先·图论
Brilliantwxx17 分钟前
【C++】模版进阶(特化+分离编译+非类型模版参数)
开发语言·数据结构·c++·算法
Black蜡笔小新18 分钟前
自动化AI算法训练服务器DLTM企业级AI模型工作站构筑企业AI自主可控新模式
人工智能·算法·自动化
bnmoel18 分钟前
数据结构深度剖析链表全集:结构实现、分类与底层原理全解析
c语言·数据结构·算法·链表·双向链表
Languorous.18 分钟前
C++数据结构高阶|跳表(Skip List)深度解析:从原理到手写实现,面试高频考点全覆盖
数据结构·c++·list
Justice Young21 分钟前
数据结构:邻接矩阵和邻接表的区别
数据结构
童先生28 分钟前
华为云、阿里云、AWS签名机制详解! AK/SK + HMAC-SHA256 签名鉴权!
算法·阿里云·华为云·云计算
承渊政道30 分钟前
【贪心算法】(经典实战应用解析(二):最⻓递增⼦序列、递增的三元⼦序列、最⻓连续递增序列、买卖股票的最佳时机、买卖股票的最佳时机II)
数据结构·c++·学习·算法·leetcode·贪心算法·哈希算法