leetcode(六) 滑动窗口、双指针与单调队列/栈

167.两数之和II (双指针)

在这里插入图片描述

明显的双指针
做了那么多双指针,其一般的写法都是具有一个从前向后和一个从后向前的指针
因为这样比较容易达成遍历O(n)的时间复杂度,而如果指针同时向一个方向移动
就容易造成时间复杂度为O(n^2)或者O(2n)的情况


做题思路
:暴力算法—找单调性—优化代码
显然这题的单调性是前指针 i 每前进一步,总和就会扩大,而后指针 j应该后退或者至少不动来使总和减小

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {

     for(int i=0,j=nums.size()-1;i<j;i++){
          
          while(nums[j]+nums[i]>target)j--;    //后指针减少到总和<=target的位置
              
          if(nums[j]+nums[i]==target)return {i+1,j+1};
   }

   return {-1,-1};
}
};


88.合并两个有序数组

在这里插入图片描述

经典归并排序
只不过这题要求合并到第一个数组里,如果不开一个新的数组,那要重新改变一下写法,考虑到第一个数组已经开辟了足够的空间,我们从大往小归并即可

//从大到小的归并
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
          int i=m-1,j=n-1,k=nums1.size()-1;      //从大到小归并,位置从大到小填空

          while(i>=0&&j>=0){
          
              if(nums1[i]<=nums2[j])nums1[k--]=nums2[j--];

              else nums1[k--]=nums1[i--];
          }

          while(j>=0)nums1[k--]=nums2[j--];   //扫尾,由于nums1[i]已经按照顺序存在于nums1内部,所以不需要扫尾
    }
};
//正常的从小到大的归并,需要开辟额外的空间
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
    
        vector<int> res;

        int i=0,j=0;

        while(i<m&&j<n){

            while(i<m&&nums1[i]<=nums2[j])res.push_back(nums1[i++]);
            

            while(j<n&&nums2[j]<nums1[i])res.push_back(nums2[j++]);
        }

        while(i<m)res.push_back(nums1[i++]);
       

        while(j<n)res.push_back(nums2[j++]);
        
        nums1=res;
    }
};

26. 删除排序数组的重复项(双指针)

在这里插入图片描述

实现一下c语言里面的unique函数
也是使用的双指针算法
前指针i,后指针j
倘若j遇到与当前i不相等的数,将其值赋给nums[++i]即可

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if(!nums.size())return 0;

        int i=0;

        for(int j=1;j<nums.size();j++)

            if(nums[j]!=nums[i])nums[++i]=nums[j];
            
            return i+1;
    }
};


76.最小覆盖子串(双指针**)

在这里插入图片描述

难度暴增,思路很好理解,写法很有技巧性,反正我想不到
思路:
首先维护一个前后指针i,j,并用哈希表对t串的所有字符个数作统计,并累计字符种类,然后维护前后指针所指的区间( i , j ),每当有符合字符入内时,哈希表累计减一,那么前指针就可以向前移动(至少不动)每当有符合字符出去时,哈希表累计加一,只要总数符合字符个数以及种类,就可以更新符合条件的区间

class Solution {
public:
    string minWindow(string s, string t) {
         unordered_map<char,int> hash;

         string res;

         for(auto item:t)hash[item]++;   //记录t里每个元素的数量

         int cnt=hash.size();            //记录t元素种类

         for(int i=0,j=0,sum=0;j<s.size();j++){   
             hash[s[j]]--;                  //遇到一个元素让其在hash表中的值-1,无用的数会被减到小于0,当然如果有用的数出现超过需求它的次数,也会被减到小于0

             if(hash[s[j]]==0)sum++;        //如果有值为0的元素出现,说明区间出现了所有这类数,

             while(hash[s[i]]<0)hash[s[i++]]++;   //尝试改变满足条件的区间,如果前指针指向的数的hash小于0,代表它是没有用的数或者是满足条件但区间内已经有足够多该数,那么把区间缩短,并且使该数在哈希中的值加一(回溯)

             if(cnt==sum){        //如果已经满足了所有种类的数,可以更新当前区间
                 if(!res.size()||j-i+1<res.size())res=s.substr(i,j-i+1);

             }
         }

         return res;
    }
};


32.最长有效括号 **(前缀和)

在这里插入图片描述

搞了半天什么是合法的括号序列
合法括号序列的重要性质
假设 ’ ( ’ 值是1 ’ ) ’ 值是-1
所有前缀和始终大于等于0,并且总和等于0时是一个合法的括号序列
如果小于0的时候要截断一次,因为小于0说明出现了)并且没有(与它匹配,相当于把当前合法的括号序列截断,需要重新统计
大于0继续做,直到找到前缀和等于0时更新答案
(((()))如果有左括号数量大于右括号的时候,找不到前缀和等于0的情况,此时我们从右到左遍历一遍,再更新答案即可

在这里插入图片描述

class Solution {
public:
    //函数实现,便于正反做一遍
    int  helper(string s){
         int res=0;

         for(int i=0,start=0,cnt=0;i<s.size();i++){
              if(s[i]=='(')cnt++;

              else{
                  cnt--;                                        //遇到')',cnt--

                  if(cnt<0)start=i+1,cnt=0;                     //如果cnt小于0,说明遇到多余的')'截断了当前连续括号序列

                  else if(cnt==0) res=max(res,i-start+1);       //当前是有效括号序列,更新答案
                }
         }

         return res;
    }
   
    int longestValidParentheses(string s) {
         int res=helper(s);

         reverse(s.begin(),s.end());   //翻转字符串

         for(auto &c:s)c^=1;            //左右括号ascll码值只差1,用异或1的方式将左括号变成右括号,右括号变成左括号,实现对称翻转

        return max(res,helper(s));     //返回正反做一遍后的最大值
    }
};


155.最小栈

在这里插入图片描述

实现一个栈,这个栈多了一种功能可以返回栈中的最小元素
方法是直接开两个栈,其中一个栈正常存储,第二个栈存储最小元素(如第一个数存前一个数的最小值,第二个数存前两个数的最小值,第三个数存前三个数的最小值…)

class MinStack {
public:
        stack<int>  stk;

        stack<int>  stk_min;
    

    /** initialize your data structure here. */
    MinStack() {
        //初始化不写
    }
    
    void push(int x) {
         stk.push(x);
         
         if(stk_min.size())stk_min.push(min(stk_min.top(),x));   //如果不为空,存放栈顶和入栈元素的较小值

         else stk_min.push(x);    //为空直接存放
    }
    
    void pop() {

        stk.pop();

        stk_min.pop();

    }
    
    int top() {

        return stk.top();

    }
    
    int getMin() {
        
        return stk_min.top();
    }
};

42.接雨水(单调栈)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

用一个res存储水滴的总量
整个过程我们维护一个从大到小的单调栈,对于每一个进栈的柱,如上图的i为例:
一旦他入栈,那么增加的水滴数量因当取决于他左边最高的柱子,我们将小于等于他的柱子全部出栈,
并且根据当前出栈柱子的高度以及上一个出栈柱子的高度last计算出每一层增加的水滴数量
(无论有没有柱子出栈)最后都要根据左边最高的柱子计算出最顶层水滴的数量,加起来就是一个柱子进栈所带来的水滴总量,循环遍历做一遍即可

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int> stk;               //递减的单调栈

        int n=height.size();

        int res=0;

        for(int i=0;i<n;i++){

            int last=0;               

            while(stk.size()&&height[stk.top()]<=height[i]){   //分层计算水滴数
                int t=stk.top();

                stk.pop();

                res+=(i-t-1)*(height[t]-last);

                last=height[t];
            }

            if(stk.size())res+=(i-stk.top()-1)*(height[i]-last); 

            stk.push(i);
        }

        return res;
    }
};

算法二

在这里插入图片描述

class Solution {
public:
    int trap(vector<int>& height) {
        int n=height.size();

        if(!n)return 0;

        vector<int > left(n),right(n);

        int q[n];

        int tt=-1,hh=0;                //因为要找每个柱子左右两边最高的矩形,所以用队列存储下标
        
       //找左边最高矩形
        for(int i=0;i<n;i++){
            
            while(hh<=tt&&height[q[tt]]<=height[i])tt--;
            
            if(hh<=tt)left[i]=height[q[hh]];

            else left[i]=0;

            q[++tt]=i;
        }

        tt=-1,hh=0;
        
        //找右边最高矩形
        for(int i=n-1;i>=0;i--){
            while(hh<=tt&&height[q[tt]]<=height[i])tt--;

            if(hh<=tt)right[i]=height[q[hh]];

            else right[i]=0;

            q[++tt]=i;

        }
       
        //对于每个矩形,如果它上面存储了水滴(>0),则统计入内
        int res=0;

        for(int i=0;i<n;i++){
            int t=min(left[i],right[i])-height[i];

            if(t>0)res+=t;
        }

        return res;
    }
};

84.柱状图中最大矩形(单调栈)

在这里插入图片描述

即对于全部的矩形,找到它的左右边界(距离它最近的最小值),计算出面积,更新最大值
找左右边界即用单调栈左右各做一次即可

在这里插入图片描述

class Solution {
public:
    int largestRectangleArea(vector<int>& h) {
            stack<int> stk;

            int n=h.size();

            vector<int> left(n),right(n);   //存储每个矩形的左右边界
            
            //找左边界
            for(int i=0;i<n;i++){

                while(stk.size()&&h[stk.top()]>=h[i])stk.pop();   //单调栈操作

                  if(stk.empty())left[i]=-1;   //如果左边界是坐标轴,默认是-1
             
                  else left[i]=stk.top();    //否则是最近的小于它的值

                  stk.push(i);               
            }

            while(stk.size())stk.pop();   //清空
           
            //倒序找右边界
            for(int i=n-1;i>=0;i--){

                while(stk.size()&&h[stk.top()]>=h[i])stk.pop();

                  if(stk.empty())right[i]=n;  //最右边界默认是n
             
                  else right[i]=stk.top();

                  stk.push(i);
            }

            int res=0;

            for(int i=0;i<n;i++)res=max(res,h[i]*(right[i]-left[i]-1));   //计算底边长为right-left-1
            
            return res;
    }
};

239.滑动窗口最大值(单调队列)

在这里插入图片描述

经典滑动窗口
通过维护一个队列来确保队头是当前窗口的最大值
时间复杂度降低到O(n)
平常使用数组来模拟的队列,由于队头和队尾都涉及到了插入删除操作,我们用deque来写这题

//数组模拟队列
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
          int n=nums.size();

          vector<int> res;

          if(!n)return {};

          int q[n];

          int tt=-1,hh=0;

          for(int i=0;i<n;i++){
             while(q[hh]<i-k+1)hh++;      //如果队头在窗口头之外应当删除

             while(hh<=tt&&nums[q[tt]]<=nums[i])tt--;   //队列有元素并且入队元素较大,删除前面的元素(使其单调递减)

             q[++tt]=i;                  
  
             if(i-k+1>=0)res.push_back(nums[q[hh]]);   //如果窗口大于等于三,输出当前最大元素
          

          return res;
        }
};


//使用双端队列
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
         int n=nums.size();

         deque<int> q;

         vector<int> res;

         for(int i=0;i<n;i++){
             while(q.front()<i-k+1&&q.size())q.pop_front();

             while(q.size()&&nums[q.back()]<=nums[i])q.pop_back();

             q.push_back(i);

             if(i-k+1>=0)res.push_back(nums[q.front()]);
         }

         return res;
    }
};

918.环形子数组的最大和(前缀和+单调队列)

在这里插入图片描述
在这里插入图片描述

  • 对于环状问题,我们将它展开为一个2n长度的链,这样做的好处是,可以从2n长度的链中找到所有的长度为1~n的子集的情况
  • 我们利用前缀和的性质解决这个问题
  • 首先求出这个数组的前缀和,对于前缀和数组,使之构成一个长度为n的滑动窗口,对于每一个入栈的前缀和si,都可以在窗口中找到最小值sj,使得si-sj的值为最大值,最后更新答案即可
  • 注意边界情况s0=0

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& A) {
          int n=A.size();

          for(int i=0;i<n;i++)A.push_back(A[i]);   //拓展为2n长的链

          deque<int> q;                           

          vector<int> sum(2*n+1);                 //前缀和通常从下标1开始(省去边界条件),所以多开一个空间

          for(int i=1;i<=2*n;i++)sum[i]=sum[i-1]+A[i-1]; //对应A的下标要错开一位

          int res=INT_MIN;

          q.push_back(0);               //枚举从sum[1]开始,提前将下标0加入deque,省去边界条件判断

          for(int i=1;i<=2*n;i++){
              while(q.size()&&q.front()<i-n)q.pop_front();

              res=max(res,sum[i]-sum[q.front()]);           //pop前先更新res的值,避免队列空而无法更新答案

              while(q.size()&&sum[q.back()]>=sum[i])q.pop_back();

              q.push_back(i);
          }


          return res;
    }
};

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值