缺失的第一个正数
问题描述
给定一个未排序的整数数组nums
,请你找出其中没有出现的最小正整数。要求实现时间复杂度为O(n),并且只使用常数级别的额外空间。
示例
-
输入:
nums = [1, 2, 0]
输出:3
解释: 范围[1, 2]
中的数字都在数组中,缺失的最小正整数为3。 -
输入:
nums = [3, 4, -1, 1]
输出:2
解释: 数组中包含1,但缺失2。 -
输入:
nums = [7, 8, 9, 11, 12]
输出:1
解释: 最小的正整数1没有出现。
输入与输出
- 输入:一个整数数组
nums
,长度在[1, 10^5]
范围内,数组中的每个元素范围为[-2^31, 2^31 - 1]
。 - 输出:一个整数,表示数组中缺失的最小正整数。
解题思路
为了在O(n)时间复杂度和常数空间复杂度下解决问题,我们可以利用数组的索引作为辅助信息。具体步骤如下:
-
原地交换:
将数组中的每个正整数放到其对应的索引位置。例如,数字1
应该放到索引0
的位置,数字2
应该放到索引1
的位置,依此类推。 -
遍历数组:
遍历数组,找到第一个不满足nums[i] == i + 1
的索引,返回i + 1
作为缺失的正整数。 -
特殊情况:
如果所有数字都正确,返回numsSize + 1
。
代码实现
以下是C语言的实现代码,包含详细注释:
int firstMissingPositive(int* nums, int numsSize) {
// 第一轮循环:将每个正整数放到其对应的索引位置
for (int i = 0; i < numsSize; i++) {
// 使用 while 循环确保每次交换后重新检查当前 nums[i]
while (nums[i] > 0 && nums[i] <= numsSize && nums[nums[i] - 1] != nums[i]) {
// 交换 nums[i] 和 nums[nums[i] - 1]
int tmp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = tmp;
}
}
// 第二轮循环:查找第一个不满足 nums[i] == i + 1 的索引
for (int i = 0; i < numsSize; i++) {
if (nums[i] != i + 1) {
return i + 1; // 返回缺失的正整数
}
}
// 如果所有数字都正确,返回 numsSize + 1
return numsSize + 1;
}
代码解析
1. 原地交换逻辑
while (nums[i] > 0 && nums[i] <= numsSize && nums[nums[i] - 1] != nums[i]) {
int tmp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = tmp;
}
- 条件解析:
nums[i] > 0 && nums[i] <= numsSize
:确保当前数字是有效的正整数,并且在数组范围内。nums[nums[i] - 1] != nums[i]
:避免重复交换,确保交换操作有意义。
- 交换逻辑:
- 使用临时变量
tmp
保存nums[nums[i] - 1]
的值。 - 将
nums[i]
放到其正确的位置nums[nums[i] - 1]
。 - 将
tmp
赋值给nums[i]
,完成交换。
- 使用临时变量
2. 查找缺失的正整数
for (int i = 0; i < numsSize; i++) {
if (nums[i] != i + 1) {
return i + 1; // 返回缺失的正整数
}
}
- 遍历数组,检查每个数字是否在其正确的位置。
- 如果发现
nums[i] != i + 1
,则返回i + 1
作为缺失的正整数。
3. 特殊情况
return numsSize + 1;
- 如果所有数字都正确,说明缺失的正整数是
numsSize + 1
。
示例分析
示例1
输入: nums = [1, 2, 0]
过程:
- 第一轮循环后,数组变为
[1, 2, 0]
(无需交换)。 - 第二轮循环中,
nums[2] = 0 != 3
,返回3
。
输出: 3
示例2
输入: nums = [3, 4, -1, 1]
过程:
- 第一轮循环后,数组变为
[1, -1, 3, 4]
。 - 第二轮循环中,
nums[1] = -1 != 2
,返回2
。
输出: 2
示例3
输入: nums = [7, 8, 9, 11, 12]
过程:
- 第一轮循环后,数组仍为
[7, 8, 9, 11, 12]
(无需交换)。 - 第二轮循环中,
nums[0] = 7 != 1
,返回1
。
输出: 1
注意事项
错误的交换顺序
int tmp = nums[i];
nums[i] = nums[nums[i] - 1];
nums[nums[i] - 1] = tmp;
问题:
在第二步中,nums[i]
被修改了,导致第三步的索引 nums[i] - 1
变成了错误的值。因为 nums[i]
已经被改过,所以后续的交换会出错。
正确的交换顺序
int tmp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = tmp;
正确原因:
在第一步中,先保存了目标位置的值 nums[nums[i] - 1]
。
在第二步中,直接把 nums[i]
放到目标位置。
在第三步中,把之前保存的值赋给 nums[i]
。
关键: 这样不会提前修改 nums[i]
,所以索引始终是正确的。
核心区别
- 错误的交换:先改了
nums[i]
,导致后续索引错乱。 - 正确的交换:先保存目标位置的值,再交换,不会错乱。
简单来说,就是不要提前修改 nums[i]
,否则索引会乱掉。
-
边界条件:
- 确保交换时的索引
nums[i] - 1
始终在有效范围内([0, numsSize - 1]
)。
- 确保交换时的索引
-
时间复杂度:
- 每个数字最多被交换两次(一次放到正确位置,一次被交换出来),因此总时间复杂度为O(n)。
总结
通过原地交换的方式,我们可以高效地将数组中的正整数放到其对应的位置,并通过一次遍历找到缺失的最小正整数。这种方法满足了O(n)时间复杂度和常数空间复杂度的要求,适用于解决此类问题。