前言
Hello 大家好!今天我们来聊聊 LeetCode 上的经典困难题——缺失的第一个正数(First Missing Positive) 。
这道题是面试中的高频考点,很多同学在看到“未排序数组”和“O(n) 时间复杂度”时,第一反应往往是排序或者使用哈希表。但今天我要带大家用一种非常巧妙、空间复杂度为 O(1) 的解法——原地哈希(置换排序) ,来完美拿下它!
题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
要求: 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例:
输入:nums = [3,4,-1,1]
输出:2
输入:nums = [7,8,9,11,12]
输出:1
核心思路:原地哈希(位置重排)
1. 关键结论:答案的范围
对于一个长度为 n 的数组,缺失的第一个正整数必然在 [1, n+1] 范围内。
- 如果数组恰好包含
1 ~ n的所有正整数,那么缺失的就是n+1(如[1,2,3]→ 缺失 4)。 - 如果数组存在
1 ~ n的缺口,那么该缺口就是答案(如[3,4,-1,1]→ 缺口为 2)。
2. 核心策略:把数组本身当哈希表
既然答案只可能在 [1, n] 或 n+1 中,我们只需要关心在这个范围内的数字。
我们可以利用数组的下标作为哈希键:
- 数字
1应该放在索引0的位置 - 数字
2应该放在索引1的位置 - 数字
k应该放在索引k-1的位置
目标: 遍历数组,将每个在 [1, n] 范围内的数 x,交换到它应该在的位置 nums[x-1] 上。
3. 为什么必须用 while 而不是 if?
这是这道题最容易踩坑的地方!
当我们把 nums[i] 交换到目标位置后,换到 i 位置的新元素可能依然不在正确的位置上。
- 如果用
if:每次只交换一次就跳过,换过来的新元素会被“晾在一边”,导致最终结果错误。 - 如果用
while:持续交换,直到当前位置的元素不在[1, n]范围内,或者已经在正确位置为止。
时间复杂度分析: 虽然看起来是双重循环,但每次
swap操作都会把至少一个元素放到正确的位置上,而每个元素最多被正确放置一次。因此总的交换次数不超过n,整体时间复杂度依然是O(n)(摊还分析)。
代码实现
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
const n = nums.length;
// 第一步:将 1~n 范围内的正整数归位
for (let i = 0; i < n; i++) {
// 循环条件:当前数在 [1,n] 范围内,且没有放在正确的位置上
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
// 将 nums[i] 交换到它应该在的位置 nums[i]-1
const temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 第二步:扫描第一个位置不对的数
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1;
}
}
// 如果 1~n 全都存在,返回 n+1
return n + 1;
};
代码细节拆解
-
循环条件三要素:
nums[i] >= 1 && nums[i] <= n:只处理有效范围内的数,负数、0、大于n的数直接忽略。nums[nums[i] - 1] !== nums[i]:防止重复数字导致死循环(比如[1,1],如果不加这个条件会无限交换)。
-
交换操作: 使用临时变量
temp完成原地交换,不引入额外空间。 -
最终扫描: 遍历归位后的数组,第一个
nums[i] !== i+1的位置,i+1就是缺失的最小正整数。
复杂度分析
- 时间复杂度:
O(n)。每个元素最多被交换到正确位置一次,总交换次数 ≤ n。 - 空间复杂度:
O(1)。只使用了常数级别的额外变量,完美满足题目要求。
总结
这道题的精髓在于打破思维定式:谁说哈希表一定要额外开辟空间?数组本身的下标就可以作为天然的哈希键!