题目描述
给你一个未排序的整数数组 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。