力扣41. 缺失的第一个正数(Java解法)

题目描述

给你一个未排序的整数数组 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。为什么?

  • 如果数组包含了 1n 的所有数字,那么答案就是 n+1

  • 否则,答案一定是 1n 中某个未出现的数字

因此,我们可以把数组本身当作哈希表来使用,将数字 i 放到下标 i-1 的位置上-9

解法一:置换法(原地交换)【最优解】

算法思想

遍历数组,对于每个元素,如果它在 [1, n] 范围内,并且它没有在正确的位置上(即 nums[i] != i+1),就把它交换到正确的位置 nums[i]-1-2-9

重复这个过程,直到当前元素要么不在范围内,要么已经在正确位置上。

代码实现

java 复制代码
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

  1. 先将所有非正数(≤0)转换为一个大于n的数(如n+1),因为它们不影响结果

  2. 遍历数组,对于每个在[1,n]范围内的数,将其对应下标位置的数标记为负数

  3. 最后遍历数组,第一个正数的下标+1就是答案

代码实现

java 复制代码
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,因为交换过来的新数可能仍然需要继续交换到正确位置。

java 复制代码
// ✅ 正确写法
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题的核心是原地哈希思想。)

常见错误与注意事项

  1. 忽略边界条件 :忘记处理 nums[i] > nnums[i] <= 0 的情况

  2. 交换逻辑错误 :使用 if 而不是 while,导致某些元素未被正确放置

  3. 索引越界nums[i] - 1 可能为负数,必须先判断 nums[i] > 0

  4. 死循环 :缺少 nums[nums[i] - 1] != nums[i] 的判断,遇到重复元素时会无限交换

  5. 忘记处理 n+1 的情况:当数组包含 1 到 n 所有数时,应返回 n+1

面试建议

  • 优先写出置换法 :这是最符合题意的标准解法,代码简洁,思路清晰-9

  • 可以提及标记法:作为对比,展示你知道多种实现方式

  • 解释为什么答案在[1, n+1] :展示你对问题的数学洞察-4

  • 强调 while 的重要性:说明为什么不能用 if,展现你对细节的把控

相关题目推荐

  • 力扣 268. 丢失的数字(原地哈希入门)

  • 力扣 442. 数组中重复的数据(原地哈希应用)

  • 力扣 448. 找到所有数组中消失的数字(标记法思路)

  • 力扣 287. 寻找重复数(类似思想)


以上就是力扣41题"缺失的第一个正数"的Java解法详细解析,这是原地哈希 思想的经典题目,重点掌握置换法这一最优解法。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!

相关推荐
颜酱2 小时前
环检测与拓扑排序:BFS/DFS双实现
javascript·后端·算法
IronMurphy2 小时前
【算法二十】 114. 寻找两个正序数组的中位数 153. 寻找旋转排序数组中的最小值
java·算法·leetcode
实心儿儿2 小时前
算法2:链表的中间结点
数据结构·算法·链表
代码探秘者2 小时前
【Java集合】ArrayList :底层原理、数组互转与扩容计算
java·开发语言·jvm·数据库·后端·python·算法
颜酱2 小时前
理解并查集Union-Find:从原理到练习
javascript·后端·算法
玛卡巴卡ldf2 小时前
【LeetCode 手撕算法】(双指针) 1-两数之和、283-移动零、11-盛最多水的容器、15-三数之和
数据结构·算法·leetcode
mygugu2 小时前
归纳理解epoch、batch、batch size、step、iteration深度学习名词
人工智能·算法
AI科技星2 小时前
基于双隐含量(角速度 +质量 )的全量变形公式体系-发现新公式
开发语言·人工智能·线性代数·算法·矩阵·数据挖掘
minji...3 小时前
Linux 基础IO (三) (用户缓冲区/内核缓冲区深刻理解)
java·linux·运维·服务器·c++·算法