从零开始写算法——普通数组篇:缺失的第一个正数

在 LeetCode 的题目中,41. 缺失的第一个正数 (First Missing Positive) 是一道非常经典的 Hard 级别题目。

它的难点不在于想出一个解法,而在于题目极其苛刻的限制条件:必须在 O(n) 的时间复杂度和 O(1) 的空间复杂度内完成

如果允许用 O(n) 的空间,我们开一个哈希表或者布尔数组就能轻松搞定;如果允许 O(n log n) 的时间,我们排序后遍历也能搞定。但在双重限制下,我们需要用到一种特殊的技巧:原地哈希(下标归位法)

一、 核心思路:一个萝卜一个坑

我们可以把数组想象成一个电影院的座位表:

  • 下标 01号 座位,应该坐数字 1

  • 下标 12号 座位,应该坐数字 2

  • ...

  • 下标 i 应该坐数字 i + 1

我们的目标很简单:遍历数组,尽量把每个数字都交换到它应该坐的座位上去。

比如拿到数字 5,我们就应该把它放到下标 4 的位置;拿到数字 x,就把它放到下标 x - 1 的位置。

对于那些负数、0、或者大于数组长度的数字(比如数组只有5个格子,你来了个数字 100),它们根本没有对应的座位,我们直接忽略,不用管它们。

当所有能入座的人都坐好后,我们从头查房。第一个没坐对人的座位,就是我们要找的缺失的数字。

二、 代码实现

这是基于 C++ 的标准实现,利用了 swap 进行原地交换。

C++代码实现

cpp 复制代码
class Solution {
    // 思路: 下标归位法, 让下标尽量归位, 比如下标0就放1, 下标1就放2, 一直交换, 并且需要对一个位置循环交换并保证交换的两个数不相等(避免死循环)
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; ++i) {
            // 核心循环:
            // 1. nums[i] >= 1 && nums[i] <= n:数字必须在合法范围内(有座位)
            // 2. nums[i] != nums[nums[i] - 1]:避免死循环,且判断目标位置是不是已经有了正确的数字
            while (nums[i] >= 1 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) {
                // 把 nums[i] 放到它该去的地方(即下标 nums[i] - 1)
                swap(nums[i], nums[nums[i] - 1]);
            }
        }
        
        // 查房阶段:看哪个位置的人不对劲
        for (int i = 0; i < n; ++i) {
            if (nums[i] != i + 1) return i + 1;
        }
        
        // 如果大家都在座位上,说明缺失的是下一个正数
        return n + 1;
    }
};

三、 细节深度解析

这段代码中最难理解,也最容易写错的就是 while 循环中的三个条件:

  1. nums[i] >= 1 && nums[i] <= n

    • 我们只关心 [1, n] 范围内的正数。负数没有座位,大于 n 的数也没座位(因为数组下标最大只到 n-1),这些数不需要(也无法)归位,直接跳过。
  2. nums[i] != nums[nums[i] - 1]

    • 这是防止死循环的关键!

    • 含义 :我们要把 nums[i] 放到目标位置 target_index = nums[i] - 1。如果目标位置上 已经 放着正确的数字了(即 nums[target_index] == nums[i]),那就不需要交换了。

    • 例子 :假设数组是 [3, 3, 1]i=1 时,nums[i] 是 3。它的目标位置是下标 2。但下标 2 的位置已经是 3 了。如果强行交换,就会一直自己换自己,导致超时。

  3. 为什么是 while 而不是 if

    • 当我们把 nums[i] 交换出去后,换回来的那个新数字可能依然是一个需要归位的正数。

    • 例如 [-1, 4, 3, 1],当 i=1 时,数字 4 换到了下标 3,把 1 换到了下标 1。此时下标 1 变成了 1,这个 1 还需要继续去下标 0。所以必须用 while 一直处理当前位置 i,直到换回来一个无法处理的废数,或者当前数字已经归位。

四、 复杂性分析

这一部分是面试官最喜欢问的,因为代码里有一个 for 循环嵌套 while 循环,很容易被误认为是 O(n^2)。

1. 时间复杂度:O(n)

虽然看起来有两层循环,但实际上是 O(n)

  • 分析视角 :不要看循环次数,看交换操作的次数

  • 道理 :每一次有效的 swap 操作,都至少会将 一个 数字放到了它最终正确的位置上(即 nums[target] = target + 1)。

  • 上限:数组里总共只有 n 个数字。一旦一个数字归位了,它就不会再被移走。所以,在整个程序的运行过程中,有效交换的总次数最多只有 n 次。

  • 因此,均摊下来,每个元素被处理的时间是常数级别的。总时间复杂度为 O(n)。

2. 空间复杂度:O(1)

  • 我们没有使用额外的哈希表(Hash Map)或者辅助数组。

  • 所有的交换和判断操作都是在原数组 nums 上进行的。

  • 只使用了常数个额外的变量(如 i, n 等)。

  • 因此,满足题目 O(1) 额外空间的要求。


五、 总结

这道题展示了如何利用 数组下标本身作为哈希表的 Key 这一技巧。

当遇到题目要求:

  1. 数据范围在 1n 之间。

  2. 要求 O(n) 时间和 O(1) 空间。

  3. 寻找缺失、重复的数字。

请第一时间想到 "原地哈希 / 下标归位法"。通过交换让每个数字回到自己的"座位",乱序的数组瞬间就变得井井有条了。

相关推荐
Forever Nore4 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考5 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX6 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面7 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号7 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽7 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI7 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者9 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者9 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-210 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵