#41 数组原地哈希 困难

缺失的第一个正数

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1

输入:nums = [1,2,0]
输出:3
范围 [1,2] 中的数字都在数组中。

示例 2

输入:nums = [3,4,-1,1]
输出:2
1 在数组中,但 2 没有。

示例 3

输入:nums = [7,8,9,11,12]
输出:1
最小的正数 1 没有出现。

💭 模拟答题者思考

1. 我先想暴力:用哈希集合记录 nums 里所有正数,再从 1 开始递增找第一个不在集合里的——正确,但需要 O(n) 额外空间,不满足题意。

2. 重复在哪里?我们其实只关心 1..n 哪些出现了;大于 n 的数和 ≤0 的数都是噪声,可以忽略。

3. 关键转化:把数组当成长度为 n 的哈希桶——值 x1≤x≤n)应该放在下标 x-1。对每个位置 i,若 nums[i] 是合法值且还没在正确位置,就与 nums[nums[i]-1] 交换,直到当前位无法继续换。

4. 例 2 [3,4,-1,1]i=0 把 3 换到 index 2 → [−1,4,3,1]i=1 把 4 换到 index 3 → [−1,1,3,4]i=1 再把 1 换到 index 0 → [1,−1,3,4]。第二遍扫描:nums[1]=−1≠2,答案 2。

5. 为什么 while 不会死循环?每次交换都让某个合法值到达最终位置,每个下标最多被「填对」一次,总交换次数 O(n)

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
nint定义:数组长度,答案只可能落在 [1, n+1]
维护:有效正整数 x 若存在,必在 1..n 内(大于 n 的数不可能是最小缺失正数)
更新:初始化后不变,用于界定「该放哪里」与最终扫描上界
iint定义:当前正在整理的数组下标
维护:从左到右扫,保证 nums[0..i-1] 已就位(nums[j]==j+1
更新:当前位置元素归位或判定为垃圾后 i++
nums[k]int定义:下标 k 处的值;语义上应存放整数 k+1(若该数存在于原数组)
维护:把每个合法值 x∈[1,n] 交换到 nums[x-1],形成「值 x 住在下标 x-1」的原地哈希表
更新:通过 swap(nums[i], nums[nums[i]-1]) 循环搬运,直到 nums[i] 不在 [1,n] 或已在正确位置
ansint定义:第一个缺失的正整数
维护:第二遍扫描找最小 i 使 nums[i] != i+1,则 ans = i+1
更新:若 1..n 全在位,ans = n+1

⌨️ 落码步骤

1. 令 n = len(nums),第一遍原地整理:对 i0n-1

2. 当 1 ≤ nums[i] ≤ nnums[i] ≠ nums[nums[i]-1] 时,交换 nums[i]nums[nums[i]-1](把 nums[i] 送到它应在的下标)

3. 内层 while 结束说明当前位已是垃圾值(≤0 或 >n)或已就位,i++ 处理下一位

4. 第二遍扫描:找最小 i 使 nums[i] ≠ i+1,返回 i+1

5. 若 1..n 全部在位,返回 n+1

💻 代码实现

class Solution:
    def firstMissingPositive(self, nums: list[int]) -> int:
        n = len(nums)
        for i in range(n):
            while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:
                j = nums[i] - 1
                nums[i], nums[j] = nums[j], nums[i]
        for i in range(n):
            if nums[i] != i + 1:
                return i + 1
        return n + 1
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[i] != nums[nums[i] - 1]) {
                int j = nums[i] - 1;
                swap(nums[i], nums[j]);
            }
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) return i + 1;
        }
        return n + 1;
    }
};
// 时间 O(N),空间 O(1)

📈 复杂度分析

时间复杂度 O(N)(每个元素最多被交换到正确位置一次)
空间复杂度 O(1)(只用常数额外变量,原地修改数组)

⚠️ 常见坑

if 只交换一次:例如 [3,4,-1,1]i=0 换完后当前位仍是 3 的「错值」,必须用 while 持续交换直到无法继续。

忘记判重 nums[i] != nums[nums[i]-1]:若目标位已是相同值(重复数字),再交换会死循环。

第二遍扫描条件写错:应比较 nums[i] != i+1,不是 nums[i] != i;下标从 0 开始,期望存放的是 i+1

🔍 必测边界 Case

Case 1:全为负数
nums = [-1,-2,-3] → 1(没有任何正数,最小缺失正数为 1)
Case 2:已连续 1..n
nums = [1,2,3] → 4(1..n 都在,答案为 n+1)
Case 3:含重复与垃圾值
nums = [3,4,-1,1] → 2(-1 与重复值被留在错误位置,不影响扫描)
Case 4:全大于 n
nums = [7,8,9,11,12] → 1(没有任何 1..n 的有效值)
Case 5:单元素
nums = [1] → 2nums = [2] → 1