力扣刷题#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仅负责文本修饰,综合获得以上内容。

posted @ 2026-06-10 14:46  STA_running  阅读(7)  评论(0)    收藏  举报