题目:
给你一个未排序的整数数组 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
题解:
如果本题没有额外的时空复杂度要求,可以用如下的哈希表方式实现。将数组所有的数放入哈希表,随后从 1 开始依次枚举正整数,并判断其是否在哈希表中。但是空间复杂度为 O(n),不符合题干要求。
cpp
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
unordered_set<int> mp;
for(int x : nums) if(x > 0) mp.insert(x);
int i = 1;
while(mp.count(i)) i++;
return i;
}
};
正确解法(原地哈希)
核心思想
利用数组本身作为哈希表:
-
答案只可能落在
[1, n+1]范围内 ,其中n = nums.size()。 -
通过修改数组元素的正负号,标记某个正整数是否出现过。
-
最终第一个正数元素的下标
i对应的值i+1即为缺失的最小正数。
为什么答案只可能在 1 ~ n+1 之间?
如果数组长度为 n,那么最理想的情况是 1,2,3,...,n 全部出现,此时缺失的是 n+1。
如果缺少某个 1~n 之间的数,那么它一定小于 n+1。
对于 ≤0 或 >n 的数字,它们不可能成为"最小的缺失正整数"
算法步骤
-
清理无关数字:把所有不在 [1, n] 范围内的数字改成 n+1(一个无意义的"大数")。
-
标记出现过的数字:遍历数组,用负号标记出现过的数字。
注意:我们只关心 1~n 这些数字是否出现过。假设某个数字 x 在 1~n 之间,那么应该把下标 x-1 位置的数变成负数。
-
查找答案:扫描数组,第一个正数所在的下标就是缺失的最小正整数
-
边界情况 :如果所有
1~n都出现过,则答案为n+1。
举例说明
以示例 nums = [3, 4, -1, 1] 为例,n = 4。
第一步:清理无关数字
-
-1≤ 0 → 改为 n+1 = 5 -
其他数字都在 1~4 之间,保持不变
结果数组:
[3, 4, 5, 1]
第二步:标记出现过的数字
遍历每个元素(取绝对值):
-
i=0, num=3 → 标记下标 2 (3-1) 的位置:
nums[2] = 5,5 > 0 吗?是 → 变为 -5。数组:[3,4,-5,1] -
i=1, num=4 → 标记下标 3 (4-1):
nums[3] = 1> 0 → 变为 -1。数组:[3,4,-5,-1] -
i=2, num=5 → 5 > n (4)?是 → 忽略
-
i=3, num=1 → 标记下标 0 (1-1):
nums[0] = 3> 0 → 变为 -3。数组:[-3,4,-5,-1]
现在负号标记了哪些下标?
-
下标 0 为负 → 数字 1 出现过
-
下标 2 为负 → 数字 3 出现过
-
下标 3 为负 → 数字 4 出现过
-
下标 1 仍然为正(4) → 数字 2 没有出现过。
第三步:查找答案
扫描:
- i=0: nums[0] = -3(负数,跳过)
- i=1: nums[1] = 4(正数) → 缺失的正整数是 i+1 = 2。
返回 2 ✅
答案:
cpp
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n=nums.size();
// 第一步:把所有不在 [1, n] 范围内的数字改成 n+1(一个无意义的"大数")
for(int &x:nums) {
if(x<=0||x>n) x=n+1;
}
// 第二步:遍历数组,用负号标记出现过的数字
// 注意:我们只关心 1~n 这些数字是否出现过。
// 假设某个数字 x 在 1~n 之间,那么应该把下标 x-1 位置的数变成负数。
for(int i=0;i<n;i++){
int num=abs(nums[i]);// 因为可能之前已被标记为负数,所以取绝对值
if(num<=n){
if(nums[num-1]>0) nums[num-1]=-nums[num-1];// 如果该位置还不是负数,就将其取反
}
}
// 第三步:扫描数组,第一个正数所在的下标就是缺失的最小正整数
for(int i=0;i<n;i++){
if(nums[i]>0) return i+1;// 下标 i 对应数字 i+1
}
// 如果所有 1~n 都出现过,则答案是 n+1
return n+1;
}
};
要点:
- 第二步中必须取绝对值
abs(nums[i]),因为前面的标记可能已经把某些位置变成了负数,但我们要读的是原始数字的值。