Leecode热题100:缺失的第一个正数(数组)

目录

回归本质,寻找物理规律

[1. 答案的范围在哪里?](#1. 答案的范围在哪里?)

[2. 存储的本质是什么?](#2. 存储的本质是什么?)

推导逻辑------原地置换

[编写 C++ 代码](#编写 C++ 代码)

面试回答

第一步:锁定答案的"物理边界"

第二步:挖掘数组的"双重身份"

第三步:推导"原地置换"的动作逻辑

第四步:判定与复杂度分析

[💡 面试加分点](#💡 面试加分点)


给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:

输入:nums = [1,2,0]

输出:3

解释:范围 [1,2] 中的数字都在数组中。

示例 2:

输入:nums = [3,4,-1,1]

输出:2

解释:1 在数组中,但 2 没有。

示例 3:

输入:nums = [7,8,9,11,12]

输出:1

解释:最小的正数 1 没有出现。

(来源:Leecode)


解决"缺失的第一个正数"是算法面试中的一座大山。它对时间 O(n) 和空间 O(1) 的严苛要求,迫使我们必须放弃所有常规武器(如哈希表或排序),直接动用第一性原理。

回归本质,寻找物理规律

1. 答案的范围在哪里?

假设数组 nums 的长度为 n。

  • 最好情况:数组包含了 1,2,3,...,n。此时,缺失的第一个正数是 n+1。

  • 其他情况:只要数组中少了 1 到 n 中的任何一个数,缺失的第一个正数必然落在 [1,n] 之间。

第一性原理结论: 缺失的答案必然在 [1,n+1] 这个闭区间内。这意味着,我们只需要关心数组中落在 [1,n] 范围内的数字,其他的负数、零或者大于 n 的数都是"噪音"。

2. 存储的本质是什么?

既然题目要求 O(1) 空间,意味着我们不能开辟新的哈希表。

第一性原理思考:数组本身就是一块内存空间。数组的索引(Index)天然就是一个哈希表的 Key。

  • 数字 1 应该放在索引 0 的位置。

  • 数字 2 应该放在索引 1 的位置。

  • 数字 i 应该放在索引 i-1 的位置。

如果我们能让这些数字"各回各家",那么最后扫描一遍数组,第一个"房主"不对的位置,就是缺失的数字。


推导逻辑------原地置换

我们要做的就是:遍历数组,如果发现数字 x 在 [1, n] 范围内,就把由它交换到它应该在的位置 nums[x-1]

核心动作:

  1. 我手里有一个数 nums[i],它的值是 x

  2. 如果 x 在 [1, n] 之间,且它没在它该在的位置上(即 nums[x-1] != x)。

  3. 我就把它和 nums[x-1] 位置上的数交换。

  4. 关键点 :交换回来的数可能也需要归位,所以我们要用 while 循环持续处理当前位置。


编写 C++ 代码

cpp 复制代码
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();

        // 第一步:原地搬运,让数字各回各家
        for (int i = 0; i < n; ++i) {
            // 只要 nums[i] 是在 [1, n] 范围内的正整数
            // 并且它没有在正确的位置上 (nums[nums[i]-1] != nums[i])
            // 就把它交换到正确的位置
            // 注意:使用 while 而不是 if,因为交换回来的新数也需要归位
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                swap(nums[i], nums[nums[i] - 1]);
            }
        }

        // 第二步:根据秩序寻找第一个乱序的位置
        for (int i = 0; i < n; ++i) {
            // 如果位置 i 上住的不是 i+1,说明 i+1 缺失了
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        // 第三步:如果 1 到 n 都在,说明缺失的是 n + 1
        return n + 1;
    }
};

面试回答

在面试中讲解这道题,最重要的是展现你如何"在极端限制(空间 O(1))下创造条件"。你可以按照"范围收窄 -> 空间挪用 -> 归位逻辑"的步骤来组织语言。


第一步:锁定答案的"物理边界"

"首先,我们要意识到这道题的答案范围是极其确定的。 对于一个长度为 n 的数组,缺失的第一个正数只可能在 [1,n+1] 这个范围内。

  • 最理想情况:数组正好是 [1,2,...,n],答案是 n+1。

  • 其他情况:只要数组中出现了负数、0、大于 n 的数,或者重复的数,那么 1 到 n 之间一定会有空档。

这个范围的收窄是解决问题的关键第一步。"


第二步:挖掘数组的"双重身份"

"题目要求 O(1) 空间,这意味着我们不能开辟哈希表。

从第一性原理出发,数组的本质就是索引到值的映射。既然我们需要记录 1 到 n 是否出现过,为什么不直接利用数组的'下标'作为哈希表的索引呢?

我们可以建立一个共识:

让数字 1 住在下标 0,数字 2 住在下标 1,以此类推,让数字 i 住在下标 i−1。这种方法通常被称为**'原地哈希'或'桶排序思想'**。"


第三步:推导"原地置换"的动作逻辑

"接下来的核心逻辑就是遍历数组,给每个数字'找家':

  1. 当我扫描到位置 i 的数字 x 时,如果 x 在 [1,n] 范围内,我就看它对应的家(即下标 x−1)是不是已经住着 x 了。

  2. 如果不是,我就把当前位置的 x 和它'家'里的那个数进行交换

  3. 关键细节:交换回来的数可能也是一个需要归位的正数。因此,我会在当前位置使用 while 循环不断进行交换,直到换回来的数不再属于 [1,n],或者它已经正确归位了。"


第四步:判定与复杂度分析

"最后,只需要再次遍历数组。第一个满足 nums[i] != i + 1 的位置,它原本该住的 i + 1 就是我们要找的答案。

关于复杂度,虽然代码里有嵌套循环,但如果我们观察**'数字归位'**这个动作:每一次有效的交换都会让一个数字回到正确位置。 数组一共只有 n 个位置,所以总交换次数最多为 n,整体时间复杂度是严格的 O(n)。"


💡 面试加分点

如果面试官追问细节,你可以补充:

  • 关于无限死循环

    "我在 while 循环的条件里,不仅判断了数值范围,还判断了 nums[i] != nums[nums[i]-1]。这非常重要,因为如果两个位置的数相同(重复数),不停交换会导致死循环。这个判断保证了重复数不会干扰逻辑。"

  • 关于索引越界

    "在访问 nums[nums[i]-1] 之前,我先判断了 nums[i] 是否在 [1, n] 之间。这种先验检查保证了程序不会出现内存访问错误。"

相关推荐
梨子串桃子_9 小时前
推荐系统学习笔记 | PyTorch学习笔记
pytorch·笔记·python·学习·算法
夏鹏今天学习了吗9 小时前
【LeetCode热题100(83/100)】最长递增子序列
算法·leetcode·职场和发展
情缘晓梦.9 小时前
C语言指针进阶
java·开发语言·算法
北邮刘老师10 小时前
智能体治理:人工智能时代信息化系统的全新挑战与课题
大数据·人工智能·算法·机器学习·智能体互联网
AlenTech11 小时前
155. 最小栈 - 力扣(LeetCode)
算法·leetcode·职场和发展
mit6.82411 小时前
正反两次扫描|单调性cut
算法
Yzzz-F11 小时前
牛客小白月赛127 E
算法
大锦终11 小时前
递归回溯综合练习
c++·算法·深度优先
Keep__Fighting11 小时前
【神经网络的训练策略选取】
人工智能·深度学习·神经网络·算法