LeetCode 41. 缺失的第一个正数
📝 题目描述
给你一个未排序的整数数组
nums,请你找出其中没有出现的最小的正整数。请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
💡 解题思路
核心观察
- 对于长度为
n的数组,答案(最小缺失正整数)只可能在[1, n+1]范围内。 - 如果
1 ~ n全都出现,那么答案就是n+1;否则答案就是1 ~ n中第一个没出现的数。
算法思想:原地哈希
我们可以利用数组下标来标记某个数是否存在。
目标 :将每个值 x(满足 1 <= x <= n)放到数组下标 x-1 的位置上,即最终让 nums[x-1] == x。
这样,处理完以后,从头扫描数组,第一个 nums[i] != i+1 的位置 i,就说明 i+1 缺失。
步骤
- 遍历数组 ,对于每个位置
i,只要nums[i]在[1, n]范围内,并且它没有放在正确的位置上,就将其与目标位置nums[i]-1上的元素交换。 - 再次遍历 ,找到第一个
nums[i] != i+1,返回i+1;如果全符合,返回n+1。
🧩 代码实现(C++)
cpp
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
// 第一步:将每个正整数放到它应该在的位置
for (int i = 0; i < n; i++) {
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
swap(nums[i], nums[nums[i] - 1]);
}
}
// 第二步:扫描第一个位置不对的数
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1)
return i + 1;
}
return n + 1;
}
};
🔍 代码详解
外层 for 循环 + 内层 while
cpp
for (int i = 0; i < n; i++) {
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
swap(nums[i], nums[nums[i] - 1]);
}
}
nums[i] >= 1 && nums[i] <= n:只关心在有效范围内的数。nums[nums[i] - 1] != nums[i]:检查目标位置是否已经放置了正确的数。如果不是,就交换。- 使用
while而不是if的原因是:交换后,新换到i位置的数可能仍然需要调整,所以要持续处理直到当前位置i不再需要移动。
为什么不会死循环?
每次交换都会把一个数放到它的最终位置(下标 nums[i]-1),一个数最多被交换一次,所以总交换次数不超过 n,整体复杂度 O(n)。
第二次扫描
cpp
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1)
return i + 1;
}
return n + 1;
- 此时数组已经"部分有序":所有
1~n内的数都被放到了对应的下标处。 - 第一个
nums[i] != i+1的位置即为答案。 - 若全部满足,说明
1~n都存在,返回n+1。
🧪 示例推演
以 nums = [3, 4, -1, 1] 为例:
| 步骤 | 操作 | 数组状态 |
|---|---|---|
| 初始 | - | [3, 4, -1, 1] |
| i=0 | nums0=3,目标下标2,交换 nums0 和 nums2 | [-1, 4, 3, 1] |
| 继续 | nums0=-1,不在1,4,退出 while | |
| i=1 | nums1=4,目标下标3,交换 nums1 和 nums3 | [-1, 1, 3, 4] |
| 继续 | nums1=1,目标下标0,交换 nums1 和 nums0 | [1, -1, 3, 4] |
| 继续 | nums1=-1,退出 while | |
| i=2 | nums2=3,已在正确位置(下标2),不交换 | |
| i=3 | nums3=4,已在正确位置(下标3),不交换 |
最终数组 [1, -1, 3, 4],扫描:
- i=0,nums0=1 ✅
- i=1,nums1=-1 ≠ 2,返回 2。
📊 复杂度分析
- 时间复杂度 :
O(n)
外层循环n次,内层while每轮至少将一个数放到正确位置,总共最多执行n次交换,因此整体线性。 - 空间复杂度 :
O(1)
只使用了若干临时变量,符合常数级别额外空间的要求。
⚠️ 注意事项
- 忽略所有小于 1 或大于
n的数,因为它们不可能放在有效下标上(答案最大为n+1),不影响最终判断。 - 重复数字的处理:如果某个位置已经正确(例如
nums[nums[i]-1] == nums[i]),即使nums[i]重复,循环条件不满足,不会发生死循环。