力扣刷题#13:LeetCode41缺失的第一个正数_刷题笔记
题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例:
输入:nums = [3, 4, -1, 1]
输出:2
输入:nums = [7, 8, 9, 11, 12]
输出:1
Step 1:先想——为什么不能用哈希表?
一看到"查找缺失的数",第一反应就是哈希表——把数组元素全丢进 unordered_set,然后从 1 开始逐个查。
但题目要求 常数级别额外空间。unordered_set 是 O(n) 空间,直接毙掉。
哈希表不让用,那还有什么办法能 O(1) 查某个数在不在?
答案:把数组本身当哈希表用。
Step 2:核心洞察——答案在什么范围内?
对于一个长度为 n 的数组,没出现的最小正整数一定在 [1, n+1] 范围内。
为什么?
- 如果
1, 2, 3, ..., n全都出现 → 答案是 n+1 - 只要缺了任何一个 → 答案就是那个缺口,肯定在
1 ~ n之间
也就是说,我们只需要关心 1 ~ n 这些数,超出范围的数压根不用管。
Step 3:把数组改成哈希表
哈希表能 O(1) 查值,是因为它把值 x 存到了位置 hash(x)。
那现在没有额外空间了,我们自己定义哈希函数:
数字 k → 位置 k-1
也就是:
- 数字 1 应该放在
nums[0] - 数字 2 应该放在
nums[1] - 数字 k 应该放在
nums[k-1]
目标:把每个在 [1, n] 范围内的数,放到它该去的位置上。
插曲:一定要"学过"才能写吗?
在推导出解法之前,我其实问了一个问题:
"我没学过原地哈希,是不是就写不出这道题?一定要我学了才能写吗?"
答案是否定的。我没有学过"原地哈希"这个概念,但我在一步步推导中把它"发明"了出来——
1. 我知道哈希表能 O(1) 查值
2. 我知道答案在 [1, n+1] 范围
3. 所以我手动定义了一个哈希函数:hash(k) = k - 1
4. 然后我把元素 swap 到对应的数组位置
这不是"没学过就不会",而是"已经会的东西组合起来,自然得到解法"。
这道题的解法不是某个你必须先学才能做题的独立知识点。它只是一个技巧——把数组本身当哈希表用。而哈希表你早就学过了,数组你也早就学过了,把它们串起来就能写出这道题。
你不需要先学"原地哈希"才能做题。你只需要会哈希表、会数组,然后面对问题自然推导就够了。
Step 4:动手实现——为什么必须用 while 而不是 if?
拿 [3, 4, -1, 1] 演示,先试 if:
i=0: swap(0,2) → [-1, 4, 3, 1] 3 到位了 ✅
i=1: swap(1,3) → [-1, 1, 3, 4] 4 到位了 ✅ 但 1 停在了位置 1...
结果:[-1, 1, 3, 4]
遍历检查:
nums[0] = -1 ≠ 1 → 返回 1 ❌ 正确答案应该是 2!
if 的问题: 每次 for 循环只 swap 一次,swap 过来的新元素如果依然不在正确位置,就会被晾在那。
现在用 while:
i=0: swap(0,2) → [-1, 4, 3, 1] while 继续
-1 不在范围 → 退出 while
i=1: swap(1,3) → [-1, 1, 3, 4] while 继续
1 ≠ nums[0] → swap(1,0) → [1, -1, 3, 4] while 继续
-1 不在范围 → 退出 while
i=2: 3 == nums[2] → 跳过
i=3: 4 == nums[3] → 跳过
结果:[1, -1, 3, 4] ✅
| 写法 | 行为 | 结果 |
|---|---|---|
if |
swap 一次就走 | 可能漏处理 |
while |
持续 swap 直到当前位置没法再处理 | 万无一失 |
最终 AC 代码
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {
// 把在 [1, n] 范围内的数放到正确位置
while (nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) {
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;
}
};
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 每个元素最多 swap 一次(见下面的摊还分析) |
| 空间复杂度 | O(1) | 只用原数组,没有额外数据结构 |
Step 5:为什么 while 的总次数是 O(n) 不是 O(n²)?
这看起来是个二重循环,但内层 while 的总次数不是 n²。
关键原因:每次 swap 都把至少一个元素送到了正确的位置,而每个元素最多被正确放一次。
nums = [3, 4, -1, 1] (n=4)
i=0: swap(0,2) → 3 到正确位置 ✅ 1次
i=1: swap(1,3) → 4 到正确位置 ✅ 1次
swap(1,0) → 1 到正确位置 ✅ 1次
i=2: 不动 0次
i=3: 不动 0次
总计:3 次 swap < n=4
在完整遍历一遍数组的过程中,所有 while 累计触发的 swap 次数 ≤ n,所以整体时间复杂度是 O(n)。
这种分析模式叫 摊还分析(Amortized Analysis)——虽然循环嵌套看起来吓人,但你把所有操作加起来算,每个元素最多被处理一次,总开销是 O(n)。
Step 6:延伸思考——这算不算一种排序?
细心观察会发现:我们的操作其实就是在排序——把每个在范围内的数放到它该去的位置。
为什么排序下限 O(n log n) 能被突破?
教科书上说的"排序最低 O(n log n)",指的是比较排序(Comparison Sort)——靠两两比较来决定顺序。
但我们现在做的不靠比较,而是利用了一个额外信息:知道每个数该去哪个位置(因为值范围是 [1, n])。
| 排序方式 | 额外信息 | 时间复杂度 |
|---|---|---|
| 比较排序(快排、归并) | 无 | O(n log n) |
| 本题解法 | 值范围 [1, n] | O(n) |
| 计数排序 | 值范围已知 | O(n + k) |
| 桶排序 | 数据分布均匀 | O(n) 平均 |
在没有任何额外信息的前提下,比较排序最低是 O(n log n)。但一旦有了额外约束,就可以突破这个下限。 算法面试中常考的就是你能不能识别出这些约束并加以利用。
总结
| 要点 | 内容 |
|---|---|
| 关键洞察 | 答案在 [1, n+1] 范围内,只需关心这些数 |
| 核心技巧 | 把数组本身当哈希表用,hash(k) = k - 1 |
| while vs if | while 保证当前位置数处理干净才离开 |
| 复杂度真相 | 每个元素最多 swap 一次 → 实际 O(n) |
| 深层价值 | 非比较排序的思维方式,利用额外约束突破理论下限 |
本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号