滑动窗口、双指针问题的一些想法

一、无重复字符的最长子串

这道题是比较经典的双指针题目,要求找到不含重复字符的最长子串,朴素想法是二重循环,找到每个区间计算。但是其中存在多余的计算,比如对一个存在重复字符的子串,包含它的子串都不符合要求,而且我们的目标是找最长的子串。
这道题里双指针的想法是,对每个右指针j,都找到其最靠左的左指针i使得区间符合条件,这样,当j每次右移的时候,i绝对不需要向左再移动,因为这样的区间不合法。

class Solution {
public:
    int  ch[10000]; 
    int lengthOfLongestSubstring(string s) {
        int res=0;
        for(int i=0,j=0;j<s.size();j++)//i,j停留在上一个i最靠左的无重复无重复子串区间
        {
            ch[s[j]]++;
            while(ch[s[j]]>1)//每次只加入一个字符,假如出现重复,一定是新加入的导致重复,因而移动i找到下一个无重复的子区间,单调性体现在i不能再向左,只能向右
            {
                ch[s[i]]--;
                i++;
            }//res能遍历到所有的无重复字符子串的情况,故正确
            res=max(res,j-i+1);

        }

        return res;
    }
};

通过这道题可以学习到双指针的基础模板,内层循环对应的是i++的情况,至于i++对应的是合法结果还是非法结果视题目而定,这里由于while循环里对应的状态非法,所以结果更新在while循环后进行;此外,这里还要学习对于变化量的处理方式,我们遇到一个新区间完全不需要判断这里面哪个字母重复,因为之前停留到上个没有重复字母的区间,所以出现重复的一定是新加入的。这样简练的思考处理方式之后也可以看到。

二、最小覆盖子串

题干要求找到s中覆盖t所有字符的最小子串,暴力方法依然是二重循环。这里考虑的优化方法是,由于每次加入一个新的字符,可能使得目前的子串满足条件,此时不需要右端点再向右移动,从而只需要缩减左端点即可,while循环的判断条件变为了维持覆盖的情况,从而找到了每个j的左边最靠右侧的满足条件的i,通过这样的思路遍历所有满足条件的子串。
判断覆盖有个比较取巧的方法,定义dist为匹配数量,只有字符在目标串中出现的数量大于目前串内的数量时,需要增加;同理,只有在当前子串中出现的数量小于等于目标串里的数量,需要减少,具体理解可以看代码。
这里while循环里结果是合法结果,因而在while里更新答案。

class Solution {
public:
    vector<int>  t_nozero;
    string minWindow(string s, string t) {
        if(t.size()>s.size())  return  "";
        vector<int> t_hash((int) 'z'+1,0);
        for(auto ti : t) { t_hash[ti]++;}
        int l=0,r=0;
        vector<int> res((int)'z'+1,0);
        int distance=0;
        int  minlen = INT_MAX,minstart=0;
        for(;r<s.size();r++)
        {
            if(res[s[r]]<t_hash[s[r]])  distance++; 
            res[s[r]]++;
            bool flag=true;
            while(distance>=t.size())
            {
                if(minlen>r-l+1)
                {
                    minlen=r-l+1;
                    minstart=l;
                }
                flag=false;
                if(res[s[l]]<=t_hash[s[l]])  distance--;
                res[s[l]]--;
                l++;
            } 
        }
        if(minlen!=INT_MAX)  return s.substr(minstart,minlen);
        return "";
    }
//之前时间主要耗费在拷贝上,实际上只要每次在while里记录下当前最短的情况即可,这样就无需之后复盘上一次是否最短
};

之前实现的较慢的方法如下,慢在判断子串的覆盖上,且结果更新放在了外层,导致判断起来较为复杂:

class Solution {
public:
    vector<int>  t_nozero;
    string minWindow(string s, string t) {
        if(t.size()>s.size())  return  "";
        vector<int> t_hash((int) 'z'+1,0);
        for(int i=0;i<t.size();i++) { t_hash[t[i]]++; t_nozero.push_back((int) t[i]);}
        int l=0,r=0;
        vector<int> res((int)'z'+1,0);
        string resstr=s;
        for(;r<s.size();r++)
        {
            res[s[r]]++;
            bool flag=true;
            while(contain(res,t_hash))
            {
                flag=false;
                res[s[l]]--;
                l++;
            } 
            if(!flag)
            {
                l--;
                res[s[l]]++; 
            }
            if(contain(res,t_hash))  resstr=(resstr.size()>r-l+1)?s.substr(l,r-l+1):resstr;
        }
        if(contain(res,t_hash))  return resstr;
        return "";
    }
    bool contain(vector<int>  res,vector<int>  t_hash)
    {
        for(int i=0;i<t_nozero.size();i++)
            if(res[t_nozero[i]]<t_hash[t_nozero[i]]) return false;
        return true;
    }
};

三、长度最小的子数组

仿照第一道题的思路可以很快写出来,每当区间值>=target时,就尝试缩小区间,直到区间值小于target。
这道题主要考虑好不成立的处理方式,如果res从来没有被更新过,说明不存在对应区间,结果为0。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int res=INT_MAX,sum=0;
        for(int i=0;i<nums.size();i++)  sum+=nums[i];
        if(sum<target)  return 0;
        sum=0;
        for(int i=0,j=0;j<nums.size();j++)
        {
            sum+=nums[j];
            while(sum>=target)
            {
                sum-=nums[i];
                i++;
                res=min(res,j-i+2);//循环内的如果是合法状态,则直接在循环里更新结果
            }
        }
       if(res==INT_MAX)  return 0;
       return res;
    }
};

通过以上几道题发现,双指针的套路都是当区间符合或者不符合结果时,更新左指针,并在这个过程中更新答案。

四、替换后的最长重复字符

这道题一开始可能想不到双指针的想法。因为对每个区间,我们都要判断其是否能够通过替换从而变为一样的字符串。最开始的思路是每次都更新一下当前区间里最多的字符数量,若剩下的字符数量比k大,表明是不合法区间,移动左指针,一直到区间满足要求即可,然后更新结果。考虑方法可以是,合法状态下,要找最长的,而i向右是缩短的情况,这时应该对应的是不满足条件的情况,直到i向右移动到合法情况,此时满足条件且长度较大。

class Solution {
public:
    int characterReplacement(string s, int k) {
        vector<int>  letter_map(128,0);
        int maxlen=0;
        for(int i=0,j=0;j<s.size();j++)
        {
            letter_map[s[j]]++;
            //find max in letter_map;
            int maxn=maxnum(letter_map);
            while(j-i+1-maxn>k)
            {
                //update max
                letter_map[s[i]]--;
                i++;
                maxn=maxnum(letter_map);
            }
            maxlen=max(maxlen,j-i+1);
        //    / cout<<i<<":"<<j<<endl;
        }
        return maxlen;
    }
    int  maxnum(vector<int>  t)
    {
        int res=t[0];
        for(int i=1;i<t.size();i++)
            res=max(res,t[i]);
        return res;
    }
};

这道题里,时间主要浪费在了更新max上,但实际上,由于每次都只增加一个新字符,因而新的max也只受该字符影响,假如该字符与之前最大值的字符不一样,则直接更新即可,假如一样的话,新的max同样是旧的max和新加入的对应字符数量求最大。总之,只有最大的max发生变化时,我们才需要考虑。

class Solution {
public:
    int characterReplacement(string s, int k) {
        vector<int>  letter_map(128,0);
       // int maxlen=0;
        int maxn=0;int i=0,j=0;
        for(;j<s.size();j++)
        {
            letter_map[s[j]]++;
            //find max in letter_map;
            maxn=max(maxn,letter_map[s[j]]);
            if(j-i+1-maxn>k)
            {
                //update max
                letter_map[s[i]]--;
                i++;
            }
           // maxlen=max(maxlen,j-i+1);
        }
        return j-i;
    }
};

不过值得注意的是,这里的while可以用if来实现,原因应该是上一个i和j停留的位置是合法的位置,对应长度已经是合法长度,而不满足条件情况下,i和j同时+1对结果不影响,因而不需要用while继续运行,否则所找的子区间长度更短,而比其更长的已经找到了。且这个过程中j和i的区间是不减的(无论是if还是while,因为j-i+1-maxn每次最多增加1),所以结果直接用j-i表示也可以。

五、找到字符串中所有字母异位词

这道题类似于之前的找覆盖子串的问题,但是相当于加了一个条件,长度等于目标串。我们可以考虑while循环条件是每个字符数量都大于目标串里对应字符的数量,这样退出循环时区间每个字符数量都小于等于目标串里对应数量,如果区间长度等于目标串,那么是合法情况。
另一种考虑方式是distance,当distance等于目标串长度时,需要减少对应长度,一直到不合法状态,这个过程中长度与目标串相同时,就是目标结果。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int>  pnum(128,0),snum(128,0);
        for(int i=0;i<p.size();i++)  pnum[p[i]]++;
        int distance=0;
        int i=0,j=0;
        vector<int> res;
        for(;j<s.size();j++)
        {
            if(snum[s[j]]<pnum[s[j]]) distance++;
            snum[s[j]]++;
            while(distance>=p.size())
            {
                if(j-i+1==p.size())  res.push_back(i);
                if(snum[s[i]]<=pnum[s[i]]) distance--;
                snum[s[i]]--;
                i++;
            }
            
        }     
        return res;
    }
};
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int>  pnum(128,0),snum(128,0);
        for(int i=0;i<p.size();i++)  pnum[p[i]]++;
        int distance=0;
        int i=0,j=0;
        vector<int> res;
        for(;j<s.size();j++)
        {
            if(snum[s[j]]<pnum[s[j]]) distance++;
            snum[s[j]]++;
            while(j-i+1>p.size())
            {
                
                if(snum[s[i]]<=pnum[s[i]]) distance--;
                snum[s[i]]--;
                i++;
            }
            if(distance==p.size())  res.push_back(i);
        }     
        return res;
    }
};
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int ht1[26] = {0}, ht2[26] = {0};
        for (auto ch : p) {
            ht2[ch - 'a']++;
        }
        vector<int> res;
        for (int slow = 0, fast = 0; fast < s.size(); fast++) {
            ht1[s[fast] - 'a']++;
            while (ht1[s[fast] - 'a'] > ht2[s[fast] - 'a']) {//每个结果串里出现的数量最后都要小于等于目标串里出现的,因为目标串里没出现的最后的区间里也不会出现,从而保证结果正确
                ht1[s[slow] - 'a']--;
                slow++;
            }
            if (fast - slow + 1 == p.size())
                res.push_back(slow);
        }
        return res;
    }
};

六、 字符串的排列

这道题和上一道题思路相同,找目标串的排列的区间,即结果区间里每个字符数量都小于等于目标串里的数量,且区间长度和目标串相同。
当然用distance同样可以判断。

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        int i=0,j=0;
        vector<int> s1map(128,0),s2map(128,0);
        for(int i=0;i<s1.size();i++)  s1map[s1[i]]++;
        for(;j<s2.size();j++)
        {
            s2map[s2[j]]++;
            while(s2map[s2[j]]>s1map[s2[j]])
            {
                s2map[s2[i]]--;
                i++;
            }
            if(j-i+1==s1.size()) return true;
        }
        return false;
    }
};

七、子数组最大平均数 I

固定长度的滑动窗口,一次循环即可,关键是用上一次的sum和本区间sum的关系简化计算。

//模板双指针
class Solution {
public:
    double findMaxAverage(vector<int>& nums, int k) {
        if(nums.size()==k)
        {
            double sum=0;
            for(int i=0;i<k;i++)  sum+=nums[i];
            return sum/k;
        }
        int res=INT_MIN;
        int sum=0;
        int i=0,j=0;
        for(;j<nums.size();j++)
        {
            sum+=nums[j];
            while(j-i+1>k)
            {
                sum-=nums[i];
                i++;
            }
            if(j-i+1==k) res=max(res,sum);
        }
        return res*1.0/k;
    }
};
class Solution {
public://标准解法,速度较快
    double findMaxAverage(vector<int>& nums, int k) {
        int sum = 0;
        int n = nums.size();
        for (int i = 0; i < k; i++) {
            sum += nums[i];
        }
        int maxSum = sum;
        for (int i = k; i < n; i++) {
            sum = sum - nums[i - k] + nums[i];
            maxSum = max(maxSum, sum);
        }
        return static_cast<double>(maxSum) / k;
    }
};

看到评论区也可以用前缀和预处理,计算方便一些。

七、 K 个不同整数的子数组

这道题比较精巧的思路在于,每个右端点都对应很多左端点区间,使得每个区间都满足k个不同整数子数组。思想是维护两个区间,一个是最靠左的最多k个不同整数子数组,另一个是最靠左的最多k-1个不同整数子数组,区间内部的就是满足k个不同整数的子数组数量。

class Solution {
public:
    int subarraysWithKDistinct(vector<int>& nums, int k) {
        int left1=0,left2=0,j=0;
        int res=0;
        vector<int>  stat1(nums.size()+1,0),stat2(nums.size()+1,0);
        int diffn1=0,diffn2=0;//这里注意每次对一个右边界,同时计算维护其最多k个不同整数的区间和最多k-1个不同整数的区间
        while(j<nums.size())
        {
            stat1[nums[j]]++;
            if(stat1[nums[j]]==1)  diffn1++;
            stat2[nums[j]]++;
            if(stat2[nums[j]]==1)  diffn2++;
            while(diffn1>k)
            {
                
                stat1[nums[left1]]--;
                if(!stat1[nums[left1]]) diffn1--;
                left1++;
            }
            while(diffn2>k-1)
            {
                
                stat2[nums[left2]]--;
                if(!stat2[nums[left2]]) diffn2--;
                left2++;
            }
            res+=left2-left1;
            j++;
        }
        return res;
    }
};


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值