

核心思路
对于长度为 n 的数组,缺失的最小正整数只可能在 [1, n+1] 范围内:
- 如果
1到n都出现了,那么答案就是n+1; - 否则,答案就是其中缺失的最小正整数。
为了满足 O (n) 时间复杂度 和 O (1) 空间复杂度 的要求,我们可以利用数组本身作为 "哈希表",将每个数 x(满足 1 ≤ x ≤ n)放到它对应的索引位置 x-1 处。最后遍历数组,第一个不满足 nums[i] == i+1 的位置 i+1 就是答案。
解题步骤
- 原地置换 :遍历数组,对于每个元素
nums[i],如果它是1到n之间的数,并且不在正确的位置(nums[i] != nums[nums[i]-1]),就将它交换到nums[i]-1的位置。 - 查找缺失 :再次遍历数组,找到第一个位置
i,使得nums[i] != i+1,返回i+1。如果所有位置都满足,返回n+1。
Java 实现
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
// 将 1~n 的数放到对应的位置
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
int temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 查找第一个缺失的正整数
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 1~n 都存在,返回 n+1
return n + 1;
}

示例验证
示例 1 :nums = [1,2,0]
- 遍历后数组为
[1,2,0]。 - 检查:
nums[0]=1,nums[1]=2,nums[2]=0 != 3,返回3。
示例 2 :nums = [3,4,-1,1]
- 遍历后数组为
[1,-1,3,4]。 - 检查:
nums[0]=1,nums[1]=-1 != 2,返回2。
示例 3 :nums = [7,8,9,11,12]
- 遍历后数组不变,所有元素都不在
1~5范围内。 - 检查:
nums[0]=7 != 1,返回1。
复杂度分析
- 时间复杂度:O (n),每个元素最多被交换到正确的位置一次,两次线性遍历。
- 空间复杂度:O (1),仅使用常数额外空间,原地修改数组。
扩展:官方题解
官方题解的思路是:
利用数组本身作为 "标记容器",通过正负号标记「1~n 范围内的数是否出现过」------ 正数表示未出现,负数表示已出现,最终第一个正数对应的索引 + 1 就是缺失的最小正整数。
逐行代码解释
class Solution {
public int firstMissingPositive(int[] nums) {
// 步骤1:获取数组长度n(核心:缺失的最小正整数只在[1, n+1]范围内)
int n = nums.length;
// 步骤2:将所有非正整数(≤0)替换为n+1(这些数对结果无影响,统一标记为"无效值")
for (int i = 0; i < n; ++i) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
// 步骤3:用"正负号"标记1~n范围内的数是否出现过
for (int i = 0; i < n; ++i) {
// 取绝对值:避免之前的标记(负号)影响判断
int num = Math.abs(nums[i]);
// 仅处理1~n范围内的数(超出的数无意义)
if (num <= n) {
// 将num对应的索引(num-1)位置的数标记为负数,表示num已出现
// 再次取绝对值:避免重复标记导致变正(比如数组有重复数时)
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
// 步骤4:遍历数组,找到第一个正数的索引,返回索引+1(即为缺失的最小正整数)
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
// 步骤5:如果数组全为负数(1~n都出现了),返回n+1
return n + 1;
}
}

关键步骤深度拆解
步骤 2:替换非正整数为 n+1
- 为什么要替换?:非正整数(0、负数)不是我们要找的 "正整数",留着会干扰后续的标记逻辑(比如负数会被误判为 "已标记");
- 为什么替换成 n+1?:n+1 是 "超出有效范围的数"(我们只关心 1~n),后续处理时会被过滤掉,不会影响标记。
步骤 3:核心标记逻辑(最关键)
- 核心公式 :数
num对应数组索引num-1(比如数 1 对应索引 0,数 3 对应索引 2); - 取绝对值的原因 :
- 第一步替换后,
nums[i]可能是正数(原正数 / 替换的 n+1),也可能是后续被标记的负数; - 必须用
Math.abs(nums[i])拿到原始数值,才能判断它是否在 1~n 范围内;
- 第一步替换后,
-Math.abs(nums[num-1])的原因 :避免重复标记导致符号反转(比如数组有重复数[2,2]):- 第一次处理
num=2:nums[1]变为负数; - 第二次处理
num=2:如果直接写nums[1] = -nums[1],会把负数变回正数,导致标记失效; - 用
-Math.abs(...)能保证无论原值是正 / 负,最终都变为负数,标记不失效。
- 第一次处理
完整示例验证(nums = [3,4,-1,1],n=4)
表格
| 步骤 | 操作 | 数组状态 | 说明 |
|---|---|---|---|
| 初始 | - | [3,4,-1,1] | 原始数组 |
| 步骤 2 | 替换≤0 的数为 5(n+1=5) | [3,4,5,1] | -1 被替换为 5 |
| 步骤 3(i=0) | num=3(≤4)→ nums[2] = -5 | [3,4,-5,1] | 标记数 3 已出现 |
| 步骤 3(i=1) | num=4(≤4)→ nums[3] = -1 | [3,4,-5,-1] | 标记数 4 已出现 |
| 步骤 3(i=2) | num=5(>4)→ 不处理 | [3,4,-5,-1] | 5 超出范围,跳过 |
| 步骤 3(i=3) | num=1(≤4)→ nums[0] = -3 | [-3,4,-5,-1] | 标记数 1 已出现 |
| 步骤 4 | 遍历找第一个正数 | 索引 1 的数是 4(正数) | 返回 1+1=2 |
最终结果为 2,和预期一致。
另一示例验证(nums = [1,2,0],n=3)
表格
| 步骤 | 操作 | 数组状态 | 说明 |
|---|---|---|---|
| 初始 | - | [1,2,0] | 原始数组 |
| 步骤 2 | 替换 0 为 4 | [1,2,4] | 0 被替换为 4 |
| 步骤 3(i=0) | num=1→nums[0]=-1 | [-1,2,4] | 标记 1 已出现 |
| 步骤 3(i=1) | num=2→nums[1]=-2 | [-1,-2,4] | 标记 2 已出现 |
| 步骤 3(i=2) | num=4>3→不处理 | [-1,-2,4] | 4 超出范围 |
| 步骤 4 | 遍历找第一个正数 | 索引 2 的数是 4(正数) | 返回 2+1=3 |
最终结果为 3,符合预期。
总结(关键点回顾)
- 核心思想:用数组索引映射 1~n 的正整数,通过正负号标记 "数是否出现",无需额外空间;
- 关键处理 :
- 替换非正整数为 n+1,避免干扰标记;
- 全程用绝对值处理,避免重复标记 / 负号干扰;
- 结果判断:第一个正数的索引 + 1 是答案,全负数则返回 n+1;
- 复杂度:时间 O (n)(三次线性遍历),空间 O (1)(原地修改),完美满足题目约束。
这种 "标记法" 是原地哈希的经典应用,和之前的 "置换法" 相比,逻辑更简洁,且避免了交换操作的边界问题,是这道题的最优解法之一。
