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. 交换归位 --- 将元素放到它应该在的位置
相关推荐
寻寻觅觅☆20 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子20 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
2013编程爱好者21 小时前
【C++】树的基础
数据结构·二叉树··二叉树的遍历
NEXT0621 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS21 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar12321 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS1 天前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗1 天前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
忙什么果1 天前
上位机、下位机、FPGA、算法放在哪层合适?
算法·fpga开发
董董灿是个攻城狮1 天前
AI 视觉连载4:YUV 的图像表示
算法