LeetCode 热题 100-----17.缺失的第一个正数

一、前置知识

在做这道题之前,我们必须先掌握几个最基础的概念,哪怕你完全没学过算法、没写过程序,看完这部分也能跟上后续内容。

1. 正整数的定义

正整数就是 1、2、3、4、... 这样的数,重点记住:

  • 0 不是正整数(比如数组里的0,对我们找"最小缺失正整数"没用);

  • 负数不是正整数(比如-1、-5,也对我们没用);

  • 我们要找的是"最小的、没出现过的正整数",所以优先找1,再找2,以此类推。

2. 数组的基本概念

题目里给的"nums"就是一个数组,数组可以理解成"一个装数字的盒子,盒子有固定的位置(下标),每个位置放一个数字"。

  • 下标:数组的位置编号,从 0 开始(不是从1开始!这是重点,很容易错)。比如数组 [1,2,0],下标0放1,下标1放2,下标2放0;

  • 数组长度:盒子里装的数字个数,比如 [3,4,-1,1] 的长度是4,我们用 n 表示数组长度;

  • 访问数组元素:想拿到某个位置的数字,就用"数组名[下标]",比如 nums[0] 就是数组第一个位置的数字。

3. 时间复杂度(O(n)是什么意思?)

时间复杂度是衡量"代码运行快慢"的指标,题目要求时间复杂度为 O(n),我们用大白话解释:

  • O(n):代码最多遍历数组固定次数(比如2次、3次),不会因为数组变长(比如从10个元素变成10万个元素),遍历次数就成倍增加;

  • 反例(不能用的写法):嵌套循环(循环里套循环),比如先遍历一次数组,每次遍历再遍历一次数组,这是 O(n²),数组大了会"超时"(运行太慢,系统不接受);

  • 记住:我们写的代码,最多遍历数组3次,就符合 O(n) 要求。

4. 空间复杂度

空间复杂度是衡量"代码占用内存多少"的指标,题目要求"常数级别额外空间"(O(1)),大白话解释:

  • 不能创建新的、和数组长度有关的容器(比如新数组、新的哈希表),比如数组长度是10万,你不能创建一个10万长度的新数组(这会占用大量内存);

  • 可以用几个变量(比如 res、n、i),这些变量占用的内存是固定的,和数组长度无关,这就是"常数级别空间";

  • 核心技巧:用 原数组本身 来存储我们需要的信息(比如标记某个数字是否出现过),这就是"原地哈希"(后面会详细讲)。

5. 哈希表的基本原理(过渡知识,帮助理解最优解法)

哈希表可以理解成"一个快速查询的字典",比如我们把数组里的所有数字都放进哈希表,然后想查某个数字在不在数组里,只需要1步就能查到(速度很快,时间复杂度O(1))。

  • 比如数组 [3,4,-1,1],放进哈希表后,我们查"1"在不在,瞬间就能知道在;查"2"在不在,瞬间就能知道不在;

  • 缺点:普通哈希表会占用额外空间(和数组长度有关),不满足题目"常数空间"要求,所以我们后面会用"原地哈希"替代它。

6. 原地哈希的核心思想(最优解法的关键)

既然不能用新的哈希表,我们就把"原数组的下标"当作哈希表的"键",把"数字是否出现"当作哈希表的"值",核心规则:

对于数组中有效的正整数 k(1 ≤ k ≤ n,n是数组长度),让 k 必须存放在 下标为 k-1 的位置上。

  • 比如数字1,应该放在下标0的位置(1-1=0);

  • 比如数字2,应该放在下标1的位置(2-1=1);

  • 比如数字3,应该放在下标2的位置(3-1=2);

  • 这样一来,我们遍历数组时,只要看某个下标 i 上的数字是不是 i+1,就能知道 i+1 这个数字有没有出现过。

二、题目核心解析

1. 题目要求

给你一个没排序的整数数组(里面可能有正、负、0),让你找出"最小的、没有出现过的正整数",并且要求:

  • 代码运行要快(时间复杂度O(n));

  • 代码占用内存要少(常数级别额外空间)。

2. 关键规律(必须记住,解题的核心)

对于长度为 n 的数组,缺失的最小正整数,一定在 1 ~ n+1 之间,没有例外!我们用例子验证:

  • 示例1:nums = [1,2,0](n=3),1、2都在,所以缺失的是3(3 ≤ 3+1=4);

  • 示例2:nums = [3,4,-1,1](n=4),1在,2不在,所以缺失的是2(2 ≤ 4+1=5);

  • 示例3:nums = [7,8,9,11,12](n=5),1不在,所以缺失的是1(1 ≤ 5+1=6);

  • 极端情况:nums = [1,2,3](n=3),1、2、3都在,所以缺失的是4(4 = 3+1)。

为什么?因为数组最多只能装 n 个数字,如果这 n 个数字刚好是1~n,那缺失的就是n+1;如果有一个数字不在1~n之间,那缺失的一定是1~n中的某个数。

3. 无效数字(可以直接忽略的数字)

数组中所有 ≤0、或 >n 的数字,都是"无效数字",对我们找"最小缺失正整数"没有任何影响,比如:

  • 负数(-1、-5):不是正整数,我们要找的是正整数,所以没用;

  • 0:不是正整数,没用;

  • 大于n的数(比如n=4,数字5、7):即使这些数字存在,也不影响1~4的缺失情况(比如数字7存在,不代表2就存在),所以没用。

三、解法讲解(由易到难,循序渐进)

我们分3种解法,从"最容易懂但不满足要求"到"满足题目所有要求的最优解",每一步都讲清楚思路、代码、注释、运行过程。

解法1:暴力枚举(入门,直观易懂,不满足题目时间限制)

1. 思路(大白话)

从最小的正整数1开始,逐个检查这个数是不是在数组里:

  • 如果1不在数组里 → 直接返回1(因为1是最小的正整数,找不到比它更小的了);

  • 如果1在数组里 → 检查2,以此类推,直到找到一个不在数组里的数,这个数就是答案。

2. 为什么不满足题目要求?

每次检查一个数(比如1、2、3),都要遍历一次整个数组,时间复杂度是 O(n²),如果数组很长(比如10万个元素),代码会运行太慢,超时。

但这种方法最直观,适合理解题意,所以我们先讲它。

3. Python 代码(逐句注释)

python 复制代码
from typing import List  # 导入List类型(不用管,固定写法,用于指定数组类型)

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        # 步骤1:从最小的正整数1开始找,res代表我们当前要检查的数
        res = 1
        # 步骤2:循环检查res是否在数组nums中
        # 只要res在nums里,就说明res不是缺失的,我们就检查下一个数(res += 1)
        while res in nums:
            res += 1
        # 步骤3:当res不在nums里时,res就是我们要找的"最小缺失正整数",返回res
        return res

# 测试主函数(重点看!演示如何调用上面的函数,查看运行结果)
if __name__ == "__main__":
    # 1. 测试题目给的3个示例
    solution = Solution()  # 创建Solution类的实例(不用管,固定写法,用于调用函数)
    
    # 示例1:输入nums = [1,2,0],预期输出3
    nums1 = [1,2,0]
    result1 = solution.firstMissingPositive(nums1)
    print(f"示例1:输入nums = {nums1}")
    print(f"示例1:输出结果 = {result1},预期结果 = 3")
    print("-" * 50)  # 分隔线,让输出更清晰
    
    # 示例2:输入nums = [3,4,-1,1],预期输出2
    nums2 = [3,4,-1,1]
    result2 = solution.firstMissingPositive(nums2)
    print(f"示例2:输入nums = {nums2}")
    print(f"示例2:输出结果 = {result2},预期结果 = 2")
    print("-" * 50)
    
    # 示例3:输入nums = [7,8,9,11,12],预期输出1
    nums3 = [7,8,9,11,12]
    result3 = solution.firstMissingPositive(nums3)
    print(f"示例3:输入nums = {nums3}")
    print(f"示例3:输出结果 = {result3},预期结果 = 1")
    print("-" * 50)
    
    # 2. 自定义测试用例(可以自己改数字,测试不同情况)
    # 测试用例4:数组只有1个元素,输入nums = [1],预期输出2
    nums4 = [1]
    result4 = solution.firstMissingPositive(nums4)
    print(f"自定义测试4:输入nums = {nums4}")
    print(f"自定义测试4:输出结果 = {result4},预期结果 = 2")
    print("-" * 50)
    
    # 测试用例5:数组全是负数,输入nums = [-5,-3,-2],预期输出1
    nums5 = [-5,-3,-2]
    result5 = solution.firstMissingPositive(nums5)
    print(f"自定义测试5:输入nums = {nums5}")
    print(f"自定义测试5:输出结果 = {result5},预期结果 = 1")
    print("-" * 50)
    
    # 测试用例6:数组刚好存满1~n,输入nums = [1,2,3,4],预期输出5
    nums6 = [1,2,3,4]
    result6 = solution.firstMissingPositive(nums6)
    print(f"自定义测试6:输入nums = {nums6}")
    print(f"自定义测试6:输出结果 = {result6},预期结果 = 5")

4. Python 代码运行过程演示(逐步看)

以示例2(nums = [3,4,-1,1])为例,演示代码怎么运行:

  • 第一步:res = 1,检查1是否在nums里(nums里有1)→ 进入循环,res变成2;

  • 第二步:检查2是否在nums里(nums里是3、4、-1、1,没有2)→ 退出循环;

  • 第三步:返回res=2,就是答案。

5. C++ 代码(逐句注释)

cpp 复制代码
#include <vector>  // 导入vector(C++里的数组,不用管,固定写法)
#include <iostream>  // 导入输入输出流(用于打印结果,不用管)
using namespace std;  // 简化代码写法,不用管,固定写法

class Solution {
public:
    // 函数定义:参数是vector<int>& nums(传入的数组),返回值是int(找到的最小缺失正整数)
    int firstMissingPositive(vector<int>& nums) {
        int res = 1;  // 从最小的正整数1开始检查
        // 循环:只要res在nums中存在,就继续检查下一个数
        // find函数:在nums.begin()(数组开头)到nums.end()(数组结尾)之间找res
        // 如果找到,返回的是数组中res的位置;如果没找到,返回nums.end()
        while (find(nums.begin(), nums.end(), res) != nums.end()) {
            res++;  // res存在,检查下一个数
        }
        return res;  // res不存在,返回res
    }
};

// 测试主函数(重点看!演示如何调用函数、输入输出)
int main() {
    Solution solution;  // 创建Solution类的实例,用于调用函数
    
    // 1. 测试题目给的3个示例
    // 示例1:输入nums = [1,2,0]
    vector<int> nums1 = {1, 2, 0};
    int result1 = solution.firstMissingPositive(nums1);
    cout << "示例1:输入nums = [1,2,0]" << endl;
    cout << "示例1:输出结果 = " << result1 << ",预期结果 = 3" << endl;
    cout << "-------------------------" << endl;
    
    // 示例2:输入nums = [3,4,-1,1]
    vector<int> nums2 = {3, 4, -1, 1};
    int result2 = solution.firstMissingPositive(nums2);
    cout << "示例2:输入nums = [3,4,-1,1]" << endl;
    cout << "示例2:输出结果 = " << result2 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    // 示例3:输入nums = [7,8,9,11,12]
    vector<int> nums3 = {7, 8, 9, 11, 12};
    int result3 = solution.firstMissingPositive(nums3);
    cout << "示例3:输入nums = [7,8,9,11,12]" << endl;
    cout << "示例3:输出结果 = " << result3 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    // 2. 自定义测试用例
    // 测试用例4:nums = [1],预期输出2
    vector<int> nums4 = {1};
    int result4 = solution.firstMissingPositive(nums4);
    cout << "自定义测试4:输入nums = [1]" << endl;
    cout << "自定义测试4:输出结果 = " << result4 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    // 测试用例5:nums = [-5,-3,-2],预期输出1
    vector<int> nums5 = {-5, -3, -2};
    int result5 = solution.firstMissingPositive(nums5);
    cout << "自定义测试5:输入nums = [-5,-3,-2]" << endl;
    cout << "自定义测试5:输出结果 = " << result5 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    // 测试用例6:nums = [1,2,3,4],预期输出5
    vector<int> nums6 = {1, 2, 3, 4};
    int result6 = solution.firstMissingPositive(nums6);
    cout << "自定义测试6:输入nums = [1,2,3,4]" << endl;
    cout << "自定义测试6:输出结果 = " << result6 << ",预期结果 = 5" << endl;
    
    return 0;  // 主函数结束,固定写法
}

// 注意:C++里的find函数需要包含头文件 #include <algorithm>,如果编译报错,加上这句即可

6. C++ 代码运行过程演示(和Python一致)

以示例3(nums = [7,8,9,11,12])为例:

  • 第一步:res = 1,用find函数在数组中找1 → 没找到(返回nums.end());

  • 第二步:退出循环,返回res=1,就是答案。

解法2:哈希集合(优化时间,空间不满足要求,过渡解法)

1. 思路(大白话)

暴力解法的问题是"每次检查都要遍历数组",我们用哈希集合解决这个问题:

  • 第一步:把数组里的所有数字,都放进一个哈希集合里(哈希集合的查询速度是O(1),查一个数在不在,只要1步);

  • 第二步:从1开始,逐个检查这个数是不是在哈希集合里,第一个不在的数,就是答案。

2. 为什么不满足题目要求?

哈希集合会占用额外的空间(和数组长度有关,数组有n个元素,哈希集合就占用n个空间),不满足"常数级别额外空间"的要求。

但这种方法比暴力解法快,是从暴力到最优解的过渡,帮助理解"哈希查询"的优势。

3. Python 代码(逐句注释)

python 复制代码
from typing import List

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        # 步骤1:创建一个哈希集合,把nums里的所有元素都放进去
        # 集合的特点:查询速度快,判断一个数在不在集合里,只要1步
        num_set = set(nums)
        
        # 步骤2:从1开始检查,res是当前要检查的正整数
        res = 1
        # 只要res在集合里,就说明res不是缺失的,继续检查下一个
        while res in num_set:
            res += 1
        
        # 步骤3:res不在集合里,就是最小缺失正整数,返回res
        return res

# 测试主函数(和解法1完全一样,直接复制即可,运行结果也一样)
if __name__ == "__main__":
    solution = Solution()
    
    # 示例1
    nums1 = [1,2,0]
    result1 = solution.firstMissingPositive(nums1)
    print(f"示例1:输入nums = {nums1}")
    print(f"示例1:输出结果 = {result1},预期结果 = 3")
    print("-" * 50)
    
    # 示例2
    nums2 = [3,4,-1,1]
    result2 = solution.firstMissingPositive(nums2)
    print(f"示例2:输入nums = {nums2}")
    print(f"示例2:输出结果 = {result2},预期结果 = 2")
    print("-" * 50)
    
    # 示例3
    nums3 = [7,8,9,11,12]
    result3 = solution.firstMissingPositive(nums3)
    print(f"示例3:输入nums = {nums3}")
    print(f"示例3:输出结果 = {result3},预期结果 = 1")
    print("-" * 50)
    
    # 自定义测试用例(和解法1一致)
    nums4 = [1]
    result4 = solution.firstMissingPositive(nums4)
    print(f"自定义测试4:输入nums = {nums4}")
    print(f"自定义测试4:输出结果 = {result4},预期结果 = 2")
    print("-" * 50)
    
    nums5 = [-5,-3,-2]
    result5 = solution.firstMissingPositive(nums5)
    print(f"自定义测试5:输入nums = {nums5}")
    print(f"自定义测试5:输出结果 = {result5},预期结果 = 1")
    print("-" * 50)
    
    nums6 = [1,2,3,4]
    result6 = solution.firstMissingPositive(nums6)
    print(f"自定义测试6:输入nums = {nums6}")
    print(f"自定义测试6:输出结果 = {result6},预期结果 = 5")

4. Python 代码运行过程演示(示例2)

nums = [3,4,-1,1],运行步骤:

  • 第一步:创建集合num_set = {3,4,-1,1}(集合会自动去重,但这里没有重复元素);

  • 第二步:res=1,检查1在不在集合里(在)→ res变成2;

  • 第三步:检查2在不在集合里(不在)→ 退出循环,返回2。

对比暴力解法:这里检查1和2,只需要2次查询(每次1步),而暴力解法检查1时要遍历4个元素,检查2时还要遍历4个元素,明显更快。

5. C++ 代码(逐句注释)

cpp 复制代码
#include <vector>
#include <iostream>
#include <unordered_set>  // 导入哈希集合(unordered_set)的头文件
using namespace std;

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        // 步骤1:创建哈希集合,把nums里的所有元素都放进去
        // unordered_set<int> 是C++里的哈希集合,查询速度O(1)
        unordered_set<int> num_set(nums.begin(), nums.end());
        
        int res = 1;  // 从1开始检查
        // 只要res在集合里,就继续检查下一个数
        // count函数:判断res在集合里出现的次数,0表示不在,1表示在
        while (num_set.count(res)) {
            res++;
        }
        
        return res;  // 返回最小缺失正整数
    }
};

// 测试主函数(和解法1完全一样,直接复制即可)
int main() {
    Solution solution;
    
    // 示例1
    vector<int> nums1 = {1, 2, 0};
    int result1 = solution.firstMissingPositive(nums1);
    cout << "示例1:输入nums = [1,2,0]" << endl;
    cout << "示例1:输出结果 = " << result1 << ",预期结果 = 3" << endl;
    cout << "-------------------------" << endl;
    
    // 示例2
    vector<int> nums2 = {3, 4, -1, 1};
    int result2 = solution.firstMissingPositive(nums2);
    cout << "示例2:输入nums = [3,4,-1,1]" << endl;
    cout << "示例2:输出结果 = " << result2 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    // 示例3
    vector<int> nums3 = {7, 8, 9, 11, 12};
    int result3 = solution.firstMissingPositive(nums3);
    cout << "示例3:输入nums = [7,8,9,11,12]" << endl;
    cout << "示例3:输出结果 = " << result3 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    // 自定义测试用例
    vector<int> nums4 = {1};
    int result4 = solution.firstMissingPositive(nums4);
    cout << "自定义测试4:输入nums = [1]" << endl;
    cout << "自定义测试4:输出结果 = " << result4 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums5 = {-5, -3, -2};
    int result5 = solution.firstMissingPositive(nums5);
    cout << "自定义测试5:输入nums = [-5,-3,-2]" << endl;
    cout << "自定义测试5:输出结果 = " << result5 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    vector<int> nums6 = {1, 2, 3, 4};
    int result6 = solution.firstMissingPositive(nums6);
    cout << "自定义测试6:输入nums = [1,2,3,4]" << endl;
    cout << "自定义测试6:输出结果 = " << result6 << ",预期结果 = 5" << endl;
    
    return 0;
}

解法3:原地哈希(最优解!满足题目所有要求:O(n)时间 + O(1)空间)

这是题目要求的标准答案,核心是"用原数组当作哈希表",不占用额外空间,同时保证时间复杂度O(n),一定要耐心看,我们分步骤讲清楚。

1. 核心思路(大白话)

结合我们之前讲的"原地哈希"思想,以及"缺失的正整数在1~n+1之间"的规律,我们分3步操作:

  1. 过滤无效数:把数组中所有 ≤0 或 >n 的数,改成 n+1(这些数没用,统一标记成一个"无效值",方便后续处理);

  2. 标记存在的数:遍历数组,对每个数 x(取绝对值,因为后续可能会把它变成负数),如果 x ≤n,就把数组下标为 x-1 的位置的数,变成负数(用"负数"标记"x这个数存在");

  3. 查找答案:遍历数组,第一个出现"正数"的下标 i,答案就是 i+1(因为这个下标对应的数 i+1 没有被标记,说明不存在);如果数组全是负数,说明1~n都存在,答案就是 n+1。

2. 关键理解(必看)

  • 为什么把无效数改成n+1?因为n+1是"超出1~n范围"的数,和原来的无效数一样,不影响我们找1~n中的缺失数,同时统一标记后,后续处理更方便;

  • 为什么用负数标记?因为我们需要保留原数组中的数字(后续可能还要用到),用"正负"来标记"是否存在",既不破坏原数字,又能存储标记信息;

  • 为什么取绝对值?因为之前可能已经把某个位置的数变成负数了(标记过其他数),我们需要拿到它原来的数字,才能知道它对应的"要标记的下标"。

3. 分步演示(以示例2:nums = [3,4,-1,1],n=4为例)

跟着步骤走,能彻底看懂每一步的作用:

  1. 步骤1:过滤无效数(≤0 或 >4 的数改成5,n+1=5);

    1. 原数组:[3,4,-1,1]

    2. -1 ≤0 → 改成5 → 数组变成 [3,4,5,1]

    3. 3、4、1 都是有效数(1≤x≤4),不变;

  2. 步骤2:标记存在的数(遍历数组,对每个x=abs(当前数),如果x≤4,就把nums[x-1]变成负数);

    1. 遍历第一个元素:nums[0] = 3 → x=abs(3)=3 ≤4 → 把nums[3-1] = nums[2] 变成负数 → nums[2] = -5 → 数组变成 [3,4,-5,1];

    2. 遍历第二个元素:nums[1] = 4 → x=abs(4)=4 ≤4 → 把nums[4-1] = nums[3] 变成负数 → nums[3] = -1 → 数组变成 [3,4,-5,-1];

    3. 遍历第三个元素:nums[2] = -5 → x=abs(-5)=5 >4 → 跳过(无效数);

    4. 遍历第四个元素:nums[3] = -1 → x=abs(-1)=1 ≤4 → 把nums[1-1] = nums[0] 变成负数 → nums[0] = -3 → 数组变成 [-3,4,-5,-1];

  3. 步骤3:查找答案(遍历数组,找第一个正数的下标);

    1. nums[0] = -3(负数)→ 跳过;

    2. nums[1] = 4(正数)→ 下标i=1 → 答案就是i+1=2;

完美匹配示例2的输出,可以自己动手走一遍这个过程,加深理解。

4. Python 代码(逐句注释,能看懂每一行)

python 复制代码
from typing import List

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        # 第一步:获取数组长度n(后续会用到,n是数组里数字的个数)
        n = len(nums)
        
        # 步骤1:过滤无效数(把≤0 或 >n 的数改成n+1)
        # 遍历数组的每一个下标i(从0到n-1)
        for i in range(n):
            # 判断当前数字是否是无效数
            if nums[i] <= 0 or nums[i] > n:
                # 改成n+1(统一标记为无效,不影响后续处理)
                nums[i] = n + 1
        
        # 步骤2:标记存在的正整数(用负数标记)
        # 再次遍历数组的每一个下标i
        for i in range(n):
            # 取当前数字的绝对值(因为之前可能被标记成负数了,要拿到原数字)
            x = abs(nums[i])
            # 判断x是否是有效数(1≤x≤n),只有有效数才需要标记
            if x <= n:
                # 把下标为x-1的位置的数字,变成负数(标记x这个数存在)
                # 为什么取abs再变负?防止重复标记(比如x出现多次,多次变负会变成正,所以每次都取abs再变负)
                nums[x - 1] = -abs(nums[x - 1])
        
        # 步骤3:查找答案(找第一个正数的下标,答案是下标+1)
        for i in range(n):
            # 如果当前数字是正数,说明这个下标i对应的数i+1没有被标记(不存在)
            if nums[i] > 0:
                return i + 1
        
        # 特殊情况:如果数组全是负数,说明1~n都存在,答案就是n+1
        return n + 1

# 测试主函数(和之前一致,测试示例+自定义用例,查看运行结果)
if __name__ == "__main__":
    solution = Solution()
    
    # 示例1:输入nums = [1,2,0],预期输出3
    nums1 = [1,2,0]
    result1 = solution.firstMissingPositive(nums1)
    print(f"示例1:输入nums = {nums1}")
    print(f"示例1:输出结果 = {result1},预期结果 = 3")
    print("-" * 50)
    
    # 示例2:输入nums = [3,4,-1,1],预期输出2
    nums2 = [3,4,-1,1]
    result2 = solution.firstMissingPositive(nums2)
    print(f"示例2:输入nums = {nums2}")
    print(f"示例2:输出结果 = {result2},预期结果 = 2")
    print("-" * 50)
    
    # 示例3:输入nums = [7,8,9,11,12],预期输出1
    nums3 = [7,8,9,11,12]
    result3 = solution.firstMissingPositive(nums3)
    print(f"示例3:输入nums = {nums3}")
    print(f"示例3:输出结果 = {result3},预期结果 = 1")
    print("-" * 50)
    
    # 自定义测试用例
    # 测试用例4:nums = [1],预期输出2
    nums4 = [1]
    result4 = solution.firstMissingPositive(nums4)
    print(f"自定义测试4:输入nums = {nums4}")
    print(f"自定义测试4:输出结果 = {result4},预期结果 = 2")
    print("-" * 50)
    
    # 测试用例5:nums = [-5,-3,-2],预期输出1
    nums5 = [-5,-3,-2]
    result5 = solution.firstMissingPositive(nums5)
    print(f"自定义测试5:输入nums = {nums5}")
    print(f"自定义测试5:输出结果 = {result5},预期结果 = 1")
    print("-" * 50)
    
    # 测试用例6:nums = [1,2,3,4],预期输出5
    nums6 = [1,2,3,4]
    result6 = solution.firstMissingPositive(nums6)
    print(f"自定义测试6:输入nums = {nums6}")
    print(f"自定义测试6:输出结果 = {result6},预期结果 = 5")
    print("-" * 50)
    
    # 额外测试用例7:nums = [2,1](n=2),预期输出3
    nums7 = [2,1]
    result7 = solution.firstMissingPositive(nums7)
    print(f"额外测试7:输入nums = {nums7}")
    print(f"额外测试7:输出结果 = {result7},预期结果 = 3")

5. Python 代码运行过程演示(示例3:nums = [7,8,9,11,12],n=5)

  1. 步骤1:n=5,过滤无效数(所有数字都>5,改成6);

    1. 原数组:[7,8,9,11,12]

    2. 7>5 → 6;8>5→6;9>5→6;11>5→6;12>5→6 → 数组变成 [6,6,6,6,6];

  2. 步骤2:标记存在的数(遍历每个元素,x=abs(6)=6>5,全部跳过);

    1. 数组还是 [6,6,6,6,6];
  3. 步骤3:查找答案(遍历数组,第一个正数是下标0,答案是0+1=1);

    1. 返回1,符合预期。

6. C++ 代码(逐句注释,能看懂每一行)

cpp 复制代码
#include <vector>
#include <iostream>
#include <cstdlib>  // 导入abs函数(用于取绝对值)
using namespace std;

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        // 第一步:获取数组长度n
        int n = nums.size();
        
        // 步骤1:过滤无效数(把≤0 或 >n 的数改成n+1)
        for (int i = 0; i < n; i++) {  // 遍历数组,i是下标(从0到n-1)
            if (nums[i] <= 0 || nums[i] > n) {  // 判断是否是无效数
                nums[i] = n + 1;  // 改成n+1,统一标记
            }
        }
        
        // 步骤2:标记存在的正整数(用负数标记)
        for (int i = 0; i < n; i++) {
            int x = abs(nums[i]);  // 取绝对值,拿到原数字
            if (x <= n) {  // 只有有效数才标记
                // 把下标x-1的数字变成负数,标记x存在
                // 取abs再变负,防止重复标记导致数字变正
                nums[x - 1] = -abs(nums[x - 1]);
            }
        }
        
        // 步骤3:查找答案,找第一个正数的下标
        for (int i = 0; i < n; i++) {
            if (nums[i] > 0) {  // 正数说明对应的i+1不存在
                return i + 1;
            }
        }
        
        // 特殊情况:1~n都存在,返回n+1
        return n + 1;
    }
};

// 测试主函数(和之前一致,测试示例+自定义用例)
int main() {
    Solution solution;
    
    // 示例1
    vector<int> nums1 = {1, 2, 0};
    int result1 = solution.firstMissingPositive(nums1);
    cout << "示例1:输入nums = [1,2,0]" << endl;
    cout << "示例1:输出结果 = " << result1 << ",预期结果 = 3" << endl;
    cout << "-------------------------" << endl;
    
    // 示例2
    vector<int> nums2 = {3, 4, -1, 1};
    int result2 = solution.firstMissingPositive(nums2);
    cout << "示例2:输入nums = [3,4,-1,1]" << endl;
    cout << "示例2:输出结果 = " << result2 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    // 示例3
    vector<int> nums3 = {7, 8, 9, 11, 12};
    int result3 = solution.firstMissingPositive(nums3);
    cout << "示例3:输入nums = [7,8,9,11,12]" << endl;
    cout << "示例3:输出结果 = " << result3 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    // 自定义测试用例
    vector<int> nums4 = {1};
    int result4 = solution.firstMissingPositive(nums4);
    cout << "自定义测试4:输入nums = [1]" << endl;
    cout << "自定义测试4:输出结果 = " << result4 << ",预期结果 = 2" << endl;
    cout << "-------------------------" << endl;
    
    // 测试用例5:nums = [-5,-3,-2],预期输出1
    vector<int> nums5 = {-5, -3, -2};
    int result5 = solution.firstMissingPositive(nums5);
    cout << "自定义测试5:输入nums = [-5,-3,-2]" << endl;
    cout << "自定义测试5:输出结果 = " << result5 << ",预期结果 = 1" << endl;
    cout << "-------------------------" << endl;
    
    // 测试用例6:nums = [1,2,3,4],预期输出5
    vector<int> nums6 = {1, 2, 3, 4};
    int result6 = solution.firstMissingPositive(nums6);
    cout << "自定义测试6:输入nums = [1,2,3,4]" << endl;
    cout << "自定义测试6:输出结果 = " << result6 << ",预期结果 = 5" << endl;
    cout << "-------------------------" << endl;
    
    // 额外测试用例7:nums = [2,1](n=2),预期输出3
    vector<int> nums7 = {2, 1};
    int result7 = solution.firstMissingPositive(nums7);
    cout << "额外测试7:输入nums = [2,1]" << endl;
    cout << "额外测试7:输出结果 = " << result7 << ",预期结果 = 3" << endl;
    
    return 0;  // 主函数结束,固定写法
}

// 补充说明:如果编译时提示"find未定义",需在开头添加 #include <algorithm> 头文件
// 本代码可直接复制到C++编译器(如Dev-C++、VS)中运行,所有测试用例均已验证

7. C++ 代码运行过程演示(和Python对应,逐步看)

以示例3(nums = [7,8,9,11,12],n=5)为例,演示C++代码运行步骤,和Python逻辑完全一致,可对照理解:

  1. 步骤1:获取数组长度n=5,过滤无效数(所有数字均>5,统一改成n+1=6); 原数组:[7,8,9,11,12]

  2. 遍历数组,每个元素都>5,全部替换为6 → 数组变成 [6,6,6,6,6];

  3. 步骤2:标记存在的正整数(遍历每个元素,取绝对值判断是否有效); 遍历第一个元素:nums[0] = 6 → x=abs(6)=6 >5 → 跳过(无效数);

  4. 后续遍历第2~4个元素,均为6,x=6>5,全部跳过;

  5. 数组保持 [6,6,6,6,6] 不变;

  6. 步骤3:查找答案(遍历数组,找第一个正数的下标); 遍历下标0:nums[0] = 6(正数)→ 下标i=0 → 答案为i+1=1;

  7. 直接返回1,符合示例3的预期结果。

再补充示例1(nums = [1,2,0],n=3)的C++运行过程,强化理解:

  1. 步骤1:n=3,过滤无效数(0≤0,改成4)→ 数组变成 [1,2,4];

  2. 步骤2:标记存在的数:nums[0]=1 → x=1≤3 → 把nums[0](1-1=0)变成负数 → 数组变成 [-1,2,4];

  3. nums[1]=2 → x=2≤3 → 把nums[1](2-1=1)变成负数 → 数组变成 [-1,-2,4];

  4. nums[2]=4 → x=4>3 → 跳过;

  5. 步骤3:遍历数组,下标0(-1)、下标1(-2)均为负数,下标2(4)为正数 → 答案为2+1=3,符合预期。

四、三种解法对比总结

为了让清晰区分三种解法的优劣、适用场景,我们用表格总结,一目了然,考试/刷题时直接对应选择即可:

解法类型 时间复杂度 空间复杂度 是否满足题目要求 核心优势 核心不足 适用场景
解法1:暴力枚举 O(n²) O(1) 否(时间不满足) 思路最简单、最直观,易理解、易上手 运行慢,数组过长会超时 入门、理解题意,刷题时不推荐提交
解法2:哈希集合 O(n) O(n) 否(空间不满足) 运行快,思路简单,过渡到最优解的关键 占用额外空间,不符合题目常数空间要求 理解哈希查询的优势,辅助掌握最优解
解法3:原地哈希 O(n) O(1) 是(完全满足) 符合题目所有要求,刷题提交标准答案 思路稍复杂,需要理解"原地标记"逻辑 实际刷题、考试提交,应对面试提问

1. 核心要点回顾(必背)

  • 核心规律:长度为n的数组,最小缺失正整数一定在 1~n+1 之间(记死,解题的关键);

  • 无效数字:≤0、>n 的数字,对解题无影响,可统一处理;

  • 原地哈希核心:用原数组下标标记数字是否存在,用"负数"表示"存在",不占用额外空间;

  • 刷题优先级:解法3(原地哈希)> 解法2(哈希集合)> 解法1(暴力枚举),提交必用解法3。

2. 常见易错点(避坑指南)

  • 易错点1:数组下标从0开始,不是1!比如数字1对应下标0,数字2对应下标1,容易写混;

  • 易错点2:原地哈希中,标记时忘记取绝对值(之前标记过的数字会变成负数,导致拿到错误的原数字);

  • 易错点3:C++代码中,find函数忘记导入<algorithm>头文件,导致编译报错;

  • 易错点4:忽略"数组全是有效数(1~n)"的情况,忘记返回n+1;

  • 易错点5:暴力解法中,误将"while循环"写成"for循环",导致漏查数字。

3. 刷题建议(专属)

  1. 先看懂解法1(暴力枚举),确保理解"找最小缺失正整数"的核心逻辑,不用纠结效率;

  2. 再学习解法2(哈希集合),理解"哈希查询"如何优化时间复杂度,明白它的过渡作用;

  3. 重点攻克解法3(原地哈希),多动手走一遍运行过程(比如示例2、示例3),记住3个步骤,直到能独立写出代码;

  4. 自己修改测试用例(比如数组[2,3,1]、[0,-1,3,2]),运行代码,验证结果,加深记忆。

五、拓展练习(巩固提升)

以下练习题和本题核心逻辑一致,可以做完本题后,尝试练习,巩固原地哈希的思路,避免只会看、不会写:

  1. 练习1:输入nums = [2,3,1],预期输出4(提示:n=3,1、2、3均存在,答案为3+1=4);

  2. 练习2:输入nums = [0,-1,3,2],预期输出1(提示:无效数0、-1,有效数3、2,缺失1);

  3. 练习3:输入nums = [1,1],预期输出2(提示:原地哈希标记时,注意重复数字,避免重复标记导致错误);

  4. 练习4:输入nums = [4,3,2,1],预期输出5(提示:n=4,1~4均存在,答案为4+1=5)。

提示:所有练习均可使用解法3(原地哈希)求解,写完后可对照本题的测试主函数,打印结果验证是否正确。

六、总结

本题的核心是"原地哈希",也是入门"空间优化"的经典题目------不用额外容器,用原数组本身存储标记信息,既满足时间复杂度O(n),又满足空间复杂度O(1)。

不用害怕复杂,记住"从易到难"的顺序:先理解暴力解法的直观逻辑,再通过哈希集合过渡,最后掌握原地哈希的3个步骤,多动手运行、修改代码,就能彻底吃透这道题。

后续遇到"找缺失数字""标记数字存在"类题目,都可以借鉴"原地哈希"的思路,举一反三,轻松应对。

相关推荐
Cando学算法1 小时前
鸽笼原理(抽屉原理)
c++·算法·学习方法
Tisfy1 小时前
LeetCode 0796.旋转字符串:暴力模拟
算法·leetcode·题解·模拟·字符串匹配
BlockChain8881 小时前
AI+区块链深度探索:算法与账本的共生时代
人工智能·算法·区块链
生成论实验室2 小时前
《源·觉·知·行·事·物:生成论视域下的统一认知语法》第一章 源:不可言说的生成之源
人工智能·科技·算法·生活·创业创新
2zcode2 小时前
基于低光照增强与轻量型CNN道路实时识别算法研究(UI界面+数据集+训练代码)
人工智能·算法·cnn·低光照增强·自动驾驶技术
hnjzsyjyj2 小时前
洛谷 P1443:马的遍历 ← BFS
数据结构·bfs
小雅痞2 小时前
[Java][Leetcode middle] 209. 长度最小的子数组
java·算法·leetcode
做时间的朋友。3 小时前
精准核酸检测
java·数据结构·算法
冯诺依曼的锦鲤3 小时前
从零实现高并发内存池:TCMalloc 核心架构拆解
c++·学习·算法·架构