在 LeetCode 的题目中,41. 缺失的第一个正数 (First Missing Positive) 是一道非常经典的 Hard 级别题目。
它的难点不在于想出一个解法,而在于题目极其苛刻的限制条件:必须在 O(n) 的时间复杂度和 O(1) 的空间复杂度内完成。
如果允许用 O(n) 的空间,我们开一个哈希表或者布尔数组就能轻松搞定;如果允许 O(n log n) 的时间,我们排序后遍历也能搞定。但在双重限制下,我们需要用到一种特殊的技巧:原地哈希(下标归位法)。
一、 核心思路:一个萝卜一个坑
我们可以把数组想象成一个电影院的座位表:
-
下标 0 是 1号 座位,应该坐数字
1。 -
下标 1 是 2号 座位,应该坐数字
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 循环中的三个条件:
-
nums[i] >= 1 && nums[i] <= n- 我们只关心
[1, n]范围内的正数。负数没有座位,大于n的数也没座位(因为数组下标最大只到n-1),这些数不需要(也无法)归位,直接跳过。
- 我们只关心
-
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 了。如果强行交换,就会一直自己换自己,导致超时。
-
-
为什么是
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到n之间。 -
要求 O(n) 时间和 O(1) 空间。
-
寻找缺失、重复的数字。
请第一时间想到 "原地哈希 / 下标归位法"。通过交换让每个数字回到自己的"座位",乱序的数组瞬间就变得井井有条了。