一、前置知识
在做这道题之前,我们必须先掌握几个最基础的概念,哪怕你完全没学过算法、没写过程序,看完这部分也能跟上后续内容。
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步操作:
-
过滤无效数:把数组中所有 ≤0 或 >n 的数,改成 n+1(这些数没用,统一标记成一个"无效值",方便后续处理);
-
标记存在的数:遍历数组,对每个数 x(取绝对值,因为后续可能会把它变成负数),如果 x ≤n,就把数组下标为 x-1 的位置的数,变成负数(用"负数"标记"x这个数存在");
-
查找答案:遍历数组,第一个出现"正数"的下标 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:过滤无效数(≤0 或 >4 的数改成5,n+1=5);
-
原数组:[3,4,-1,1]
-
-1 ≤0 → 改成5 → 数组变成 [3,4,5,1]
-
3、4、1 都是有效数(1≤x≤4),不变;
-
-
步骤2:标记存在的数(遍历数组,对每个x=abs(当前数),如果x≤4,就把nums[x-1]变成负数);
-
遍历第一个元素:nums[0] = 3 → x=abs(3)=3 ≤4 → 把nums[3-1] = nums[2] 变成负数 → nums[2] = -5 → 数组变成 [3,4,-5,1];
-
遍历第二个元素:nums[1] = 4 → x=abs(4)=4 ≤4 → 把nums[4-1] = nums[3] 变成负数 → nums[3] = -1 → 数组变成 [3,4,-5,-1];
-
遍历第三个元素:nums[2] = -5 → x=abs(-5)=5 >4 → 跳过(无效数);
-
遍历第四个元素:nums[3] = -1 → x=abs(-1)=1 ≤4 → 把nums[1-1] = nums[0] 变成负数 → nums[0] = -3 → 数组变成 [-3,4,-5,-1];
-
-
步骤3:查找答案(遍历数组,找第一个正数的下标);
-
nums[0] = -3(负数)→ 跳过;
-
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:n=5,过滤无效数(所有数字都>5,改成6);
-
原数组:[7,8,9,11,12]
-
7>5 → 6;8>5→6;9>5→6;11>5→6;12>5→6 → 数组变成 [6,6,6,6,6];
-
-
步骤2:标记存在的数(遍历每个元素,x=abs(6)=6>5,全部跳过);
- 数组还是 [6,6,6,6,6];
-
步骤3:查找答案(遍历数组,第一个正数是下标0,答案是0+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:获取数组长度n=5,过滤无效数(所有数字均>5,统一改成n+1=6); 原数组:[7,8,9,11,12]
-
遍历数组,每个元素都>5,全部替换为6 → 数组变成 [6,6,6,6,6];
-
步骤2:标记存在的正整数(遍历每个元素,取绝对值判断是否有效); 遍历第一个元素:nums[0] = 6 → x=abs(6)=6 >5 → 跳过(无效数);
-
后续遍历第2~4个元素,均为6,x=6>5,全部跳过;
-
数组保持 [6,6,6,6,6] 不变;
-
步骤3:查找答案(遍历数组,找第一个正数的下标); 遍历下标0:nums[0] = 6(正数)→ 下标i=0 → 答案为i+1=1;
-
直接返回1,符合示例3的预期结果。
再补充示例1(nums = [1,2,0],n=3)的C++运行过程,强化理解:
-
步骤1:n=3,过滤无效数(0≤0,改成4)→ 数组变成 [1,2,4];
-
步骤2:标记存在的数:nums[0]=1 → x=1≤3 → 把nums[0](1-1=0)变成负数 → 数组变成 [-1,2,4];
-
nums[1]=2 → x=2≤3 → 把nums[1](2-1=1)变成负数 → 数组变成 [-1,-2,4];
-
nums[2]=4 → x=4>3 → 跳过;
-
步骤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(暴力枚举),确保理解"找最小缺失正整数"的核心逻辑,不用纠结效率;
-
再学习解法2(哈希集合),理解"哈希查询"如何优化时间复杂度,明白它的过渡作用;
-
重点攻克解法3(原地哈希),多动手走一遍运行过程(比如示例2、示例3),记住3个步骤,直到能独立写出代码;
-
自己修改测试用例(比如数组[2,3,1]、[0,-1,3,2]),运行代码,验证结果,加深记忆。
五、拓展练习(巩固提升)
以下练习题和本题核心逻辑一致,可以做完本题后,尝试练习,巩固原地哈希的思路,避免只会看、不会写:
-
练习1:输入nums = [2,3,1],预期输出4(提示:n=3,1、2、3均存在,答案为3+1=4);
-
练习2:输入nums = [0,-1,3,2],预期输出1(提示:无效数0、-1,有效数3、2,缺失1);
-
练习3:输入nums = [1,1],预期输出2(提示:原地哈希标记时,注意重复数字,避免重复标记导致错误);
-
练习4:输入nums = [4,3,2,1],预期输出5(提示:n=4,1~4均存在,答案为4+1=5)。
提示:所有练习均可使用解法3(原地哈希)求解,写完后可对照本题的测试主函数,打印结果验证是否正确。
六、总结
本题的核心是"原地哈希",也是入门"空间优化"的经典题目------不用额外容器,用原数组本身存储标记信息,既满足时间复杂度O(n),又满足空间复杂度O(1)。
不用害怕复杂,记住"从易到难"的顺序:先理解暴力解法的直观逻辑,再通过哈希集合过渡,最后掌握原地哈希的3个步骤,多动手运行、修改代码,就能彻底吃透这道题。
后续遇到"找缺失数字""标记数字存在"类题目,都可以借鉴"原地哈希"的思路,举一反三,轻松应对。