有序数组平方后不重复元素个数

该博客讨论了一种算法问题,即如何计算一个有序数组在对其元素平方后不重复的数字数量。算法使用了双指针技术,通过维护一个`pre`指针来跟踪前一个元素的绝对值,比较左右指针所指向元素的绝对值,并根据比较结果更新计数`cnt`。左右指针逐步向中间收敛,直到它们相遇,最后返回计数结果。

题目:有序数组{-9,-3,-1,1,1,3,9,9}平方之后{81,9,1,1,1,9,81,81}不重复的数字有多少,题目中3个

思路:需要2个移动指针,和一个pre指针

需要和pre比较,不相等,则res+1

左右指针分别向中间移动

int uniqueCnt(vector<int> inputs){
    int left=0,right=inputs.size()-1,cnt=0;
    int pre=-1;
    while(left<right){

        if(abs(inputs[left])>abs(inputs[right])){
            if(pre!=abs(inputs[left])){
                cnt++;
                pre=abs(inputs[left]);
            }
            left++;
        }else{
            if(pre!=abs(inputs[right])){
                cnt++;
                pre=abs(inputs[right]);
            }
            right--;
        }
    }
    return cnt;
}

<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$ 小乘积的二分查找解法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值