<think>
我们有一个递增有序的整数数组A,长度为n。我们需要找到未出现的最小正整数。
注意:数组是递增有序的,但可能包含负数、0和正数。我们需要的是最小未出现的正整数(从1开始)。
思路:
1. 由于数组是有序的,我们可以利用这个性质来减少查找时间。
2. 最小未出现的正整数一定在1到n+1之间(为什么呢?因为如果1到n都出现了,那么最小未出现的就是n+1;如果中间有缺失,那么最小未出现的就是缺失的那个数)。
3. 但是注意:数组可能有负数和非正整数,所以我们需要跳过这些数。
具体步骤:
方法一:遍历数组,忽略负数,然后从1开始检查是否连续出现。但这样最坏情况是O(n),但可以利用有序性提前终止。
方法二:二分查找。我们可以利用数组有序的特性,通过二分查找来加速。
如何使用二分查找?
我们希望在数组中找到第一个使得A[i]不等于i+1的正整数位置(假设我们只考虑正数部分,并且忽略重复的情况)。但是数组中有负数,我们需要先找到第一个正数的位置。
另一种思路:
1. 先找到第一个大于等于1的位置(即跳过负数和0)。
2. 然后从这个位置开始,我们希望数组中的元素是连续的正整数,即A[i]等于某个起始值(比如1)开始连续递增。但是数组可能有重复,所以跳过重复。
但是题目要求未出现的最小正数,所以我们需要找到第一个位置i(从0开始计数,但只考虑正数部分),使得从第一个正数位置开始,连续正数部分不是从1开始连续递增的。
具体二分思路(假设我们找到了第一个非负数的位置,记为left):
我们希望在子数组A[left...n-1]中,找到最小的缺失正整数。
注意:这个子数组是递增的,并且都是正整数(或者0,但我们已经跳过了负数,0我们也要跳过?题目要求正数,所以0不算正数,最小正数是1)。
实际上,我们可以这样考虑:假设数组A中从位置left开始的元素都是正整数,且递增。那么我们希望在这个子数组中找到第一个不满足A[i] == i - left + 1的数(这里i是从left开始的下标,我们希望从1开始连续,所以位置left的元素应该是1?不一定,因为可能前面有缺失,比如数组是[2,3,4]那么最小缺失正数是1)。
所以,我们的目标其实是:在数组中找到1,如果没有1,那么答案就是1。如果有1,那么我们接着找2,3...直到某个数缺失。
由于数组有序,所以我们可以这样做:
遍历正数部分,设一个变量cur=1,然后跳过重复元素(因为数组可能重复),然后逐个检查:
如果A[i] == cur,则cur++,i++(并且跳过重复的,即如果下一个等于当前A[i]则跳过)。
如果A[i] > cur,则说明cur缺失,答案就是cur。
但是这种方法最坏情况是O(n),而二分查找能否优化?
二分查找的另一种思路(利用索引和值的关系):
我们考虑数组的下标(从0到n-1),我们希望能够利用下标来二分。我们设定一个条件:对于下标i(从0开始),如果A[i] <= i+1,那么缺失的数一定在i+1之后吗?不一定。
实际上,我们可以这样定义:
如果数组前k个正数(从第一个正数开始)包含了1到k,那么最小的缺失正数至少是k+1(注意这里k是正数的个数,并且要求连续包含1到k)。
但是我们不知道第一个正数的位置,所以不好直接二分。
改进:
我们可以将数组下标重新映射:假设第一个正数的位置是left,那么从left开始的第0个位置(即left)应该对应正整数1,第1个位置(left+1)对应2,以此类推。
那么,对于下标j(从left到n-1),它对应的期望的正整数是j-left+1。
我们希望找到一个最小的j(从left到n-1),使得A[j] > j-left+1,那么缺失的最小正整数就是j-left+1(因为从left到j-1都满足A[i]<=i-left+1?实际上,如果连续,那么应该有A[i]==i-left+1吗?不一定,因为可能有重复或者跳跃)。
但是注意:数组可能有重复,所以即使A[j]等于j-left+1,也可能后面有重复,所以我们需要确保每个数都被检查。
然而,由于数组是递增的,所以我们可以保证:如果A[j] >= j-left+1,那么前面的数一定都满足A[i] >= i-left+1(因为数组递增),但是不一定是连续的。
因此,我们可以这样:
在区间[left, n-1]上,我们二分枚举一个位置mid,计算mid对应的期望值:expected = mid - left + 1。
如果A[mid] < expected,那么说明这个位置的值小于期望值,但是因为是递增的,所以后面可能会出现期望值?但是实际上,由于数组递增且都是整数,所以如果A[mid] < expected,那么前面的数一定更小,所以不可能出现期望值了(因为期望值是从1开始递增的)?这不可能,因为前面的数如果缺失,那么可能后面某个位置的值大于期望值。
所以这个条件不好判断。
鉴于数组长度为n,直接遍历正数部分的时间复杂度为O(n),而n不会太大(或者很大,但二分查找能优化到O(log n)),我们可以考虑另一种二分思路:
我们二分的是答案x(最小未出现的正整数),x的范围为[1, n+1]。
那么如何判断一个数x是否出现?可以在数组中进行二分查找(因为数组有序,所以查找x的时间为O(log n)),但是这样我们需要对每个x都二分查找,总时间O(n log n),不好。
或者,我们统计小于等于x的元素个数,设为cnt,如果cnt>=x,说明1到x都出现了?不对,因为小于等于x的元素个数>=x,并不能说明1到x都出现了(可能重复,也可能中间有跳跃)。所以这个条件也不行。
重新思考:我们可以遍历数组,但是只遍历正数部分,并且使用一个变量记录当前期望的正整数(从1开始)。由于数组有序,所以重复的数字是连续的,跳过重复即可。
算法步骤:
1. 初始化一个变量miss=1(最小未出现的正整数从1开始)。
2. 遍历数组的每个元素(从下标0到n-1):
如果当前元素小于miss,则跳过(负数或者小于miss的正数,不会影响miss)。
如果当前元素等于miss,则说明miss出现了,那么将miss加1(然后继续检查当前元素后面重复的部分,因为数组递增,重复元素会连续出现,所以我们需要跳过重复元素?但是我们在遍历过程中,已经按顺序遍历了,所以重复元素会连续出现,我们可以在等于miss时,将miss加1,然后继续遍历,重复元素再次等于miss(此时miss已经增加了)就不会再等于了,所以需要跳过重复?比如[1,1,2],当遇到第一个1时,miss变为2,然后遇到第二个1,这时第二个1小于2吗?1<2,所以跳过?这里我们可以这样:当当前元素小于miss时跳过,所以第二个1会被跳过,然后遇到2,等于miss(2),然后miss变为3。这样也可以。所以我们不需要显式跳过重复,因为重复元素要么等于miss(但miss已经增加了,所以不会再次等于,除非重复元素连续出现且等于miss之前的数),所以当当前元素小于miss时跳过,包括重复元素也会被跳过。
但是,如果数组是[1,1,3]呢?
遍历第一个1:等于miss(1),所以miss=2。
遍历第二个1:此时1<2,所以跳过。
遍历3:大于miss(2),那么说明2缺失,所以返回2。
所以算法如下:
int miss = 1;
for (int i=0; i<n; i++) {
if (nums[i] <= 0) continue; // 跳过非正数
// 当nums[i]大于等于miss时才有意义
if (nums[i] > miss) {
// 说明miss未出现,直接返回miss
return miss;
} else if (nums[i] == miss) {
miss++;
}
// 如果nums[i] < miss,说明是重复元素(因为之前已经出现过,所以miss已经增加过了)或者小于当前期望的数,跳过。
}
return miss;
但是注意:数组遍历顺序是递增的,所以遇到大于miss的数,说明miss缺失,因为后面的数都更大,不可能再出现miss了。
但是,如果没有提前返回,那么遍历结束后,返回miss。
这个算法的时间复杂度是O(n),因为只遍历一次数组。
但是,题目给了数组有序,我们能否利用二分查找优化到O(log n)?实际上,上面的算法已经是O(n)了,而二分查找在数组有序时通常可以优化到O(log n),但是这里我们需要考虑的是最小未出现的正整数,它可能和前面的所有元素都有关联(连续性),所以直接二分查找位置可能困难。
不过,我们可以考虑二分查找第一个使得当前位置的期望值与实际值不匹配的位置。
重新定义:我们设f(i)为从数组开始位置到位置i(包括i)已经出现了1到f(i)的所有数(连续)。那么当我们遍历到位置i时,f(i) = max{ f(i-1), A[i] }? 不对,因为连续性要求。
实际上,我们可以这样:在位置i(从0开始),我们只关心正数部分,设我们跳过了所有负数,然后从第一个正数开始,位置索引为j0,j1,...,那么对于第k个正数(k从0开始),它对应的期望值是k+1。我们希望找到第一个k使得A[j_k] > k+1,那么缺失的最小正整数就是k+1。
因此,我们可以在正数部分(连续递增)上做二分:在正数部分中,我们寻找第一个满足A[j] > k+1的位置k(注意k是正数序列中的下标,从0开始),那么缺失的最小正整数就是k+1。
具体步骤:
1. 找到第一个非负数的位置left(即第一个>=0的位置,因为0不算正数,所以我们可以找第一个>=1的位置?但是最小未出现正整数可能是1,所以1缺失的话,不管后面有没有0都不影响。所以我们可以直接找第一个>=1的位置,因为负数和非正数都不影响最小正数)。
2. 设low=0, high=n-left(即正数部分的长度),然后二分查找mid(在正数部分中的下标,实际位置为left+mid),期望的正整数为mid+1。
3. 比较A[left+mid]和mid+1:
如果A[left+mid] == mid+1,说明mid+1出现,那么继续在右边找(因为我们希望找到第一个不满足的)。
如果A[left+mid] > mid+1,说明mid+1缺失(因为如果前面连续,那么到mid这个位置应该期望是mid+1,但现在实际值大于期望值,说明期望值缺失),那么缺失的数在左边或就是mid+1?但是注意,由于数组递增,前面的位置的值一定小于等于A[left+mid],所以可能前面已经缺失了某个数,所以我们应该在左半部分查找(因为我们要找的是最小的缺失正整数,所以如果当前位置的期望值缺失,那么最小的缺失值一定小于等于mid+1)。
如果A[left+mid] < mid+1?这种情况不可能,因为数组递增,且我们跳过了负数,但是A[left+mid]至少为1(因为第一个正数>=1),而mid+1至少为1(mid>=0),那么为什么会有小于呢?注意,我们定义的是正数部分,所以应该都是>=1的。而且由于数组递增,所以前面的正数部分一定都小于等于后面的。但是,可能有重复,比如[1,1,3],在第一个正数(位置0)A[0]=1,期望1,满足;第二个正数(位置1)A[1]=1,此时mid=1(即第二个正数),期望为2,但是实际值为1(<2),所以这种情况是存在的(重复元素导致实际值小于期望值)。
所以,我们需要处理重复元素:重复元素会导致实际值小于期望值,但我们不能认为缺失已经发生(因为缺失的是期望值,而重复元素并没有提供新的期望值)。我们只有在实际值大于期望值时,才能确定期望值缺失(因为实际值后面的数更大,所以期望值不可能再出现)。而重复元素的情况,我们需要跳过(相当于这个重复元素没有提供新的期望值,所以期望值不变)。
因此,如果我们在二分查找中遇到重复元素(A[left+mid] < mid+1)时,我们只能说明这个重复元素没有提供新的期望值,所以我们应该在右半部分查找?不对,因为重复元素不影响,我们继续向后找,可能后面有更大的数来满足期望值。所以,重复元素不应该影响二分的条件,我们需要的是第一个满足A[left+mid] > mid+1的位置。
但是,重复元素会导致多个位置的实际值等于同一个较小的期望值(比如第一个位置期望1,实际1;第二个位置期望2,实际1,所以第二个位置实际值1<期望2),那么我们要找的位置应该是第一个实际值大于期望值的位置(因为在此之前,期望值都没有超过实际值,而重复元素不会增加期望值)。
所以,我们二分查找的目标是:第一个满足A[left+mid] > mid+1的位置mid(在正数部分的下标)。那么最小缺失正整数就是mid+1(因为mid位置的期望值是mid+1,而实际值大于它,说明mid+1缺失,且前面都连续)。
为什么前面连续?因为我们二分查找的是第一个不满足的位置,所以前面的位置都满足A[i] <= i+1(注意,重复元素满足A[i]<=i+1,因为重复元素的值小于期望值,所以满足<=,但不一定连续)。但是,我们要求是连续出现1,2,...,mid,如果前面有重复,那么期望值会停滞,所以实际值可能等于前面的期望值,不会大于,直到遇到一个数大于当前期望值,此时当前期望值缺失。
所以,算法如下:
1. 找到第一个大于等于1的位置left(即第一个正数的位置)。
2. 如果left==n(数组全为负数),那么最小未出现的正整数就是1。
3. 如果A[left] > 1,那么最小未出现正整数就是1(因为1缺失)。
4. 否则,设置low=0, high=n-left-1(因为正数部分长度为len = n-left,下标范围[0, len-1]),然后我们在[0, len-1]中查找第一个满足A[left+mid] > mid+1的mid(注意mid是正数部分的下标,从0开始)。
5. 如果找不到这样的mid(即所有位置都满足A[left+mid] <= mid+1),那么最小未出现正整数就是len+1(因为正数部分长度为len,且连续出现了1到len,那么最小未出现的就是len+1);如果找到了,那么最小未出现正整数就是mid+1(因为mid是第一个不满足的位置,期望值mid+1缺失)。
但是,注意重复元素:重复元素不会大于期望值,所以重复元素不会触发条件,因此我们需要考虑重复元素是否会导致期望值增加缓慢。实际上,我们二分查找的条件是:A[left+mid] > mid+1。重复元素(A[left+mid] <= mid+1)会被归到左边,然后我们向右查找。
但是,我们如何保证第一个满足A[left+mid] > mid+1的位置mid,其期望值mid+1就是缺失的最小正整数?因为前面的位置可能已经缺失了某个数?不,因为二分查找会找到第一个不满足的位置,而前面的每个位置都满足A[i] <= i+1(这里i是正数部分的下标),那么1到mid+1-1(即mid)都出现了吗?不一定,因为重复元素导致期望值增加缓慢,可能前面的期望值并没有达到mid+1。所以我们需要重新考虑。
实际上,我们想利用数组有序性来跳过重复元素,但是二分查找无法有效处理重复元素对期望值增加的阻碍。
因此,我们回到线性扫描的方法,虽然时间复杂度为O(n),但简单且正确。
线性扫描代码:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
int miss = 1;
// 先排序?题目已经有序,所以不需要再排序。但我们直接按顺序遍历。
// 注意:数组是递增有序的,所以重复元素连续出现。
for (int i=0; i<n; i++) {
if (nums[i] <= 0) continue; // 跳过非正数
// 当nums[i]大于miss,说明miss缺失(因为数组有序,后面不可能再出现miss)
if (nums[i] > miss) {
return miss;
} else if (nums[i] == miss) {
miss++;
}
// 重复元素:如果nums[i] < miss,说明这个数是重复的(因为已经处理过,所以miss已经大于它),跳过
}
return miss;
}
但是,这个代码在遇到重复正数时,比如[1,1,2,3]:
第一个1:等于miss(1) -> miss=2
第二个1:小于miss(2) -> 跳过
2:等于miss(2) -> miss=3
3:等于miss(3) -> miss=4
返回4
实际上,最小未出现的正整数是4,因为1,2,3都出现了。
但是如果是[1,1,3]:
第一个1:miss=2
第二个1:跳过
3:大于miss(2) -> 返回2,正确。
所以这种方法正确。
但是,题目要求数组是递增有序,所以重复元素是连续的,所以不会出现[1,2,1,3]这样的输入。
但是,注意:题目只说了递增有序,但可能有重复吗?题目没有说无重复,所以可能有重复。
因此,我们可以用这个线性扫描算法。
但是,有没有可能优化到O(log n)?实际上,在特定情况下可以,但最坏情况还是需要O(n)(比如数组全是1,那么我们一定要扫描到最后一个1才能确定miss=2)。所以一般情况下,线性扫描已经是最优,因为我们需要考虑每个元素(至少是正数部分)。
不过,我们可以尝试提前结束:当数组的当前元素已经大于miss,就可以返回miss了,后面的元素都不需要再检查(因为数组递增,后面的都比miss大)。所以最坏情况是数组的所有正数部分都连续且无重复(或者重复不影响,比如[1,2,3,...,n]),那么我们需要遍历整个正数部分,O(n)。但平均情况下,如果缺失发生较早,可以提前结束。
因此,我们采用线性扫描。
但是,还有一个问题:数组可能没有正数?那么miss一开始为1,遍历结束后返回1。
所以算法完整。
代码实现:
#include <vector>
using namespace std;
int firstMissingPositive(vector<int>& nums) {
int miss = 1;
for (int num : nums) {
if (num <= 0) continue; // 跳过非正数
if (num > miss) {
return miss;
} else if (num == miss) {
miss++;
}
// 如果num < miss,说明是重复的正数(因为之前已经出现过,所以miss已经超过了),跳过
}
return miss;
}
测试用例:
[1,2,3,4,5] -> 6
[0,1,2] -> 3 (注意0跳过,然后1,2出现,所以miss变成3,然后返回3)
[0,2,3] -> 1 (因为第一个正数2>1,所以返回1)
[1,1,1] -> 2 (第一个1:miss=2;第二个1:小于2,跳过;第三个1:小于2,跳过;返回2)
[2,3,4] -> 1
但是,这个算法有一个前提:数组有序递增吗?题目给定是递增有序,所以我们可以这样用。
但如果数组不是有序的,这个方法就不行。不过题目要求是有序数组。
所以,我们编写代码。
但是,注意题目要求:数组是递增有序(升序)的,所以可以保证重复元素连续,且后面的数不会比前面小,所以我们可以放心使用。
因此,我们给出代码:
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int miss = 1;
for (int num : nums) {
if (num <= 0) continue;
// 如果当前数大于miss,则找到缺失的数
if (num > miss) {
return miss;
} else if (num == miss) {
miss++;
}
// 当num小于miss时,跳过(重复的数)
}
return miss;
}
};
但是,我们考虑一个边界:如果数组为空?
n=0,则循环不执行,返回1,正确。
所以,这个算法可以满足要求。
但是,我们再看题目要求:递增有序数组,但我们的算法并没有显式跳过负数,而是遇到负数就跳过。在递增数组中,负数一定在开头,所以当我们遇到第一个正数时,后面的数都是正数(因为递增),所以我们可以提前结束负数的跳过?但是不影响,因为负数跳过很快。
因此,我们提供这个代码。
另外,题目要求生成相关问题,我们生成三个问题:
相关问题:
1. 如果数组无序,如何找到未出现的最小正整数?(要求时间复杂度O(n),空间复杂度O(1))
2. 如果数组中有重复元素,如何优化查找未出现的最小正整数?
3. 如何在一个包含正数、负数和零的未排序数组中找到缺失的第一个正整数?
注意:我们的算法是建立在有序的基础上的,所以如果无序,就需要其他方法。
因此,我们输出代码和问题。
</think>
### 查找递增有序数组中未出现的最小正整数
#### 问题分析
给定一个长度为 `n` 的递增有序整数数组 `A`,要求找到数组中未出现的最小正整数。数组可能包含负数、零和重复元素,但已按升序排序。
#### 算法思路
1. **核心思想**:利用数组有序的特性,遍历数组并维护一个变量 `miss`,表示当前期望的最小正整数(初始为1)。
2. **跳过非正数**:遇到负数或零时直接跳过。
3. **处理正数**:
- 若当前元素等于 `miss`,说明该数已出现,将 `miss` 加1。
- 若当前元素大于 `miss`,说明 `miss` 未出现,直接返回 `miss`(因为数组有序,后续元素更大)。
- 若当前元素小于 `miss`,说明是重复元素,跳过。
4. **遍历结束**:若数组所有元素均不满足返回条件,则返回 `miss`(说明缺失的是最后一个连续正数后的值)。
#### 时间复杂度
- **最优情况**:缺失值位于数组前部(如 `[2,3,4]`),时间复杂度为 $O(1)$。
- **最坏情况**:需遍历整个数组(如 `[1,2,3,...,n]`),时间复杂度为 $O(n)$。
- 平均时间复杂度 $O(n)$,空间复杂度 $O(1)$。
#### C++代码实现
```cpp
#include <vector>
using namespace std;
int firstMissingPositive(vector<int>& nums) {
int miss = 1; // 初始化最小未出现正整数为1
for (int num : nums) {
if (num <= 0) continue; // 跳过非正数
if (num > miss) return miss; // 找到缺失数
else if (num == miss) miss++; // 更新期望值
// 重复元素自动跳过(num < miss)
}
return miss; // 遍历结束返回当前miss
}
```
#### 测试用例
| 输入数组 | 输出 | 说明 |
|------------------|------|-------------------------------|
| `[1,2,3,4]` | 5 | 连续无缺失,返回n+1 |
| `[0,2,3,5]` | 1 | 1缺失 |
| `[-1,1,2,4]` | 3 | 3缺失 |
| `[1,1,3,4]` | 2 | 重复元素导致2缺失 |
| `[-3,-1,0,2]` | 1 | 1缺失且含负数 |
| `[ ]` | 1 | 空数组返回1 |
#### 关键点解释
1. **数组有序的作用**:确保遍历时遇到的数单调递增,当 `num > miss` 时可直接返回(后续数更大)。
2. **重复元素处理**:通过 `num < miss` 自动跳过重复值。
3. **边界处理**:空数组返回1;全负数数组返回1;连续正数返回 `n+1`。
---
### 相关问题
1. **无序数组的缺失最小正整数**
若数组无序,如何以 $O(n)$ 时间复杂度和 $O(1)$ 空间复杂度找到未出现的最小正整数?例如,给定 `[3,4,-1,1]`,返回 `2`[^1]。
2. **大量重复元素的优化**
当数组中有大量重复元素时(如 `[1,1,1,2,2,100]`),如何优化查找未出现的最小正整数(保持 $O(n)$ 时间复杂度)?[^1]
3. **扩展:两个有序数组的第 $k$ 小乘积**
给定两个递增数组,如何高效找到所有乘积中第 $k$ 小的值?例如,`nums1 = [-2,-1]`, `nums2 = [1,2]`,$k=1$ 时返回 $-4$[^4]。
[^1]: 引用站内技术文章:二分查找算法在有序数据结构中的应用。
[^2]: 引用站内技术文章:有序数组平方问题的双指针解法。
[^3]: 引用站内技术文章:LeetCode 977 题的分治策略实现。
[^4]: 引用站内技术文章:两个有序数组第 $k$ 小乘积的二分查找解法。