题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中,缺失的第一个正数是3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1在数组中,但2没有出现
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数1没有出现
提示:
-
1 <= nums.length <= 5 * 10^5
-
-2^31 <= nums[i] <= 2^31 – 1
解题思路
这道题是「原地哈希」的经典难题,难点在于O(n)时间复杂度和常数级额外空间的双重约束-2。
关键洞察
对于一个长度为 n 的数组,缺失的第一个正数一定在 [1, n+1] 范围内-2-4。为什么?
-
如果数组包含了 1 到 n 的所有数字,那么答案就是 n+1
-
否则,答案一定是 1 到 n 中某个未出现的数字
因此,我们可以把数组本身当作哈希表来使用,将数字 i 放到下标 i-1 的位置上-9。
解法一:置换法(原地交换)【最优解】
算法思想
遍历数组,对于每个元素,如果它在 [1, n] 范围内,并且它没有在正确的位置上(即 nums[i] != i+1),就把它交换到正确的位置 nums[i]-1 上-2-9。
重复这个过程,直到当前元素要么不在范围内,要么已经在正确位置上。
代码实现
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 1. 将每个数字放到它应该在的位置
for (int i = 0; i < n; i++) {
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] – 1] != nums[i]) {
// 将 nums[i] 交换到下标为 nums[i]-1 的位置
swap(nums, i, nums[i] – 1);
}
}
// 2. 遍历数组,找到第一个位置 i 上数字不是 i+1 的
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 3. 如果所有位置都正确,返回 n+1
return n + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
图解示例
以 nums = [3, 4, -1, 1] 为例:
| 初始 | [3, 4, -1, 1] | i=0,nums[0]=3,应在下标2 |
| i=0交换 | [-1, 4, 3, 1] | 交换 nums[0] 和 nums[2] |
| i=0 | [-1, 4, 3, 1] | nums[0] = -1,不在范围内,跳过 |
| i=1 | [-1, 4, 3, 1] | nums[1]=4,应在下标3,交换 |
| i=1交换 | [-1, 1, 3, 4] | 交换 nums[1] 和 nums[3] |
| i=1 | [-1, 1, 3, 4] | nums[1]=1,应在下标0,交换 |
| i=1交换 | [1, -1, 3, 4] | 交换 nums[1] 和 nums[0] |
| i=1 | [1, -1, 3, 4] | nums[1] = -1,跳过 |
| i=2 | [1, -1, 3, 4] | nums[2]=3,已在正确位置 |
| i=3 | [1, -1, 3, 4] | nums[3]=4,已在正确位置 |
最终数组:[1, -1, 3, 4],第一个错误的位置是下标1(应为2,实际是-1),所以返回 1+1=2 ✓
复杂度分析
-
时间复杂度: O(n),每个元素最多被交换一次-9
-
空间复杂度: O(1),原地修改,只使用了常数个临时变量
解法二:标记法(负数标记)
算法思想
另一种实现方式是用负数做标记-3-8:
先将所有非正数(≤0)转换为一个大于n的数(如n+1),因为它们不影响结果
遍历数组,对于每个在[1,n]范围内的数,将其对应下标位置的数标记为负数
最后遍历数组,第一个正数的下标+1就是答案
代码实现
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 1. 将所有非正数改为 n+1(一个不会影响结果的数)
for (int i = 0; i < n; i++) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
// 2. 对于每个在[1,n]范围内的数,标记其对应下标位置
for (int i = 0; i < n; i++) {
int num = Math.abs(nums[i]);
if (num <= n) {
// 将下标 num-1 处的数标记为负数,表示数字num存在
nums[num – 1] = -Math.abs(nums[num – 1]);
}
}
// 3. 找到第一个正数的下标
for (int i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
}
图解示例
以 nums = [3, 4, -1, 1] 为例:
| 初始 | – | [3, 4, -1, 1] |
| 1 | 负值标记 | [3, 4, 5, 1] (-1→n+1=5) |
| 2 | 处理3:下标2标记为负 | [3, 4, -5, 1] |
| 2 | 处理4:下标3标记为负 | [3, 4, -5, -1] |
| 2 | 处理5:5>n,忽略 | [3, 4, -5, -1] |
| 2 | 处理1:下标0标记为负 | [-3, 4, -5, -1] |
| 3 | 找正数:下标1是正数4 | 返回 i+1 = 2 ✓ |
解法对比
| 哈希表法 | O(n) | O(n) | 思路直观 | 不满足空间要求-2 |
| 排序+二分 | O(n log n) | O(1) | 思路简单 | 不满足时间要求-2 |
| 置换法 | O(n) | O(1) | 最优解,思路清晰 | 需要理解交换逻辑 |
| 标记法 | O(n) | O(1) | 避免交换操作 | 需要处理负数标记 |
代码要点详解
1. 为什么用 while 而不是 if?
在置换法中,我们使用 while 循环而不是 if,因为交换过来的新数可能仍然需要继续交换到正确位置。
// ✅ 正确写法
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] – 1] != nums[i]) {
swap(nums, i, nums[i] – 1);
}
// ❌ 错误写法 – 只能处理一次交换
if (nums[i] > 0 && nums[i] <= n && nums[nums[i] – 1] != nums[i]) {
swap(nums, i, nums[i] – 1);
}
2. 交换条件判断
交换条件 nums[nums[i] – 1] != nums[i] 是为了避免死循环-2。如果两个位置的值相等,交换没有意义,应该跳过。
3. 处理重复元素
当数组中有重复元素时,上述条件也能正确处理。例如 [2, 2]:
-
i=0:nums[0]=2,下标1的值也是2,条件不成立,不交换
-
最终数组 [2, 2],下标0应为1,所以返回1 ✓
为什么不能用除法?—— 本题不涉及
(温馨提示:本题不涉及除法,这是之前238题的考点。41题的核心是原地哈希思想。)
常见错误与注意事项
忽略边界条件:忘记处理 nums[i] > n 或 nums[i] <= 0 的情况
交换逻辑错误:使用 if 而不是 while,导致某些元素未被正确放置
索引越界:nums[i] – 1 可能为负数,必须先判断 nums[i] > 0
死循环:缺少 nums[nums[i] – 1] != nums[i] 的判断,遇到重复元素时会无限交换
忘记处理 n+1 的情况:当数组包含 1 到 n 所有数时,应返回 n+1
面试建议
-
优先写出置换法:这是最符合题意的标准解法,代码简洁,思路清晰-9
-
可以提及标记法:作为对比,展示你知道多种实现方式
-
解释为什么答案在[1, n+1]:展示你对问题的数学洞察-4
-
强调 while 的重要性:说明为什么不能用 if,展现你对细节的把控
相关题目推荐
-
力扣 268. 丢失的数字(原地哈希入门)
-
力扣 442. 数组中重复的数据(原地哈希应用)
-
力扣 448. 找到所有数组中消失的数字(标记法思路)
-
力扣 287. 寻找重复数(类似思想)
以上就是力扣41题"缺失的第一个正数"的Java解法详细解析,这是原地哈希思想的经典题目,重点掌握置换法这一最优解法。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!

![打卡信奥刷题(3665)用C++实现信奥题 P12195 [NOISG 2025 Prelim] Itinerary-171主机测评](https://www.171host.com/wp-content/uploads/2026/11/20261101084528-6ae6fc28235d6-220x150.png)