题目:
给你一个未排序的整数数组 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, n+1]
- 交换归位 --- 将元素放到它应该在的位置