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

在 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. 寻找缺失、重复的数字。

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

相关推荐
Nebula_g2 小时前
线程进阶: 无人机自动防空平台开发教程(更新)
java·开发语言·数据结构·学习·算法·无人机
rit84324992 小时前
基于MATLAB的环境障碍模型构建与蚁群算法路径规划实现
开发语言·算法·matlab
hoiii1872 小时前
MATLAB SGM(半全局匹配)算法实现
前端·算法·matlab
独自破碎E2 小时前
大整数哈希
算法·哈希算法
纤纡.2 小时前
逻辑回归实战进阶:交叉验证与采样技术破解数据痛点(二)
算法·机器学习·逻辑回归
czhc11400756632 小时前
协议 25
java·开发语言·算法
范纹杉想快点毕业2 小时前
状态机设计与嵌入式系统开发完整指南从面向过程到面向对象,从理论到实践的全面解析
linux·服务器·数据库·c++·算法·mongodb·mfc
fish-man2 小时前
测试加粗效果
算法
晓13133 小时前
第二章 【C语言篇:入门】 C 语言基础入门
c语言·算法