滑动窗口专题

通过以下几道题来熟悉滑动窗口
滑动窗口3大问题:如何移入窗口,如何移出窗口,如何更新答案

209. 长度最小的子数组

 我们考虑通过窗口来计算和,快慢指针从左开始遍历。

移入窗口:直接把当前元素加进来。
移出窗口:如果和大于target就把左边移出,看看窗口变小了还是否满足。
更新答案:在移出窗口前记录当前长度与之前的结果取min。

简单分析一下这样是否一定能得出正确答案:假设答案是数组中的某一段,那么这个答案一定是从砍去左端一点一点移出来的,也就是我们会找到最终结果。

代码如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        int sum = 0;
        int ant = INT_MAX;//存答案
        for (int i = 0, j = 0; i < n; i++) {
            sum += nums[i];//移入窗口
            while (i >= j && sum >= target) {
                //当前窗口满足题意了,看看变小是否满足
                ant = min(ant, i - j + 1);//更新答案
                sum -= nums[j++];//移出窗口
            }
        }
        return ant == INT_MAX ? 0 : ant;//ant没变过说明整个数组所有值加起来也到不了target,返回0
    }
};

3. 无重复字符的最长子串

跟上一题有点像,上一题为最短,这题为最长。

我们想一下如何用滑动窗口3步来实现

移入窗口:把当前字符放入窗口
移出窗口:窗口中有重复字符时,移动左端,直到无重复字符
更新答案:无重复字符就与之前存的结果取max

简单分析一下这样是否一定能得出正确答案:假设答案是字符串中的某一段,那么这个答案左边第一个一定与窗口内字符相重,该操作也是我们移出窗口能得到的,也就是我们会找到最终结果。

代码如下:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> m;//统计窗口内字符种类和数量
        int n = s.size();
        int ant = 0; //答案
        for (int i = 0, j = 0; i < n; i++) {
            m[s[i]]++;//移入窗口
            while (j <= i && m[s[i]] > 1) {
                m[s[j]]--;//有重复的移出窗口
                j++;
            }
            ant = max(ant, i - j + 1);//更新答案
        }
        return ant;
    }
};

76. 最小覆盖子串

这个题难度就来了,我们看看它比前两个难在哪,又该怎么处理

我们想一下,首先得遍历字符串t,看看它有多少种字符,每种字符有多少个。

然后遍历字符串s看看,哪一段满足且是最短的。如果s能覆盖t,那最长肯定是s,最短的情况是中间的某一段,也是可以通过窗口移出来的。

移入窗口:当前字符放进来,看看这个字符是否覆盖
移出窗口:如果所有字符都覆盖则移出左端看看是否还覆盖
更新答案:如果所有字符都覆盖就统计一下左右端点,取长度最小,最后截取一下字符串即可 

代码如下:

class Solution {
public:
    string minWindow(string s, string t) {
        int ls = s.size();
        unordered_map<char, int> ms, mt;
        int left = -1, right = 100005; //存答案
        for (auto i : t)
            mt[i]++;  //遍历一下看看每种字符有多少个
        int num = mt.size();//算算有多少种字符未被覆盖
        for (int i = 0, j = 0; i < ls; i++) {

            if (++ms[s[i]] == mt[s[i]])//当前字符加入窗口
                num--;  //如果该字符被覆盖了则减1
            while (j <= i && num == 0) {//全覆盖了
                if (right - left + 1 > i - j + 1)//比较长度
                    left = j, right = i;//更新答案
                if (--ms[s[j]] < mt[s[j]])
                    num++;//移出窗口,移出后不覆盖则num++
                j++;
            }
        }
 return right - left > ls ? "" : s.substr(left, right - left + 1);
 //right-left为初始值则返回空字符串,否则截取所需字符串
    }
};

这道题主要多了一步移入移出额外判断 

134. 加油站

这道题用了环形数组,我们采用倍增来处理,并依然可以采用滑动窗口来解决。

我们想一下如果从某站开始无法绕一圈,那一定会在中间停下,这时我们只需把起始点去掉,从它的下一点开始看看能不能绕一圈,这就蕴含滑窗的思想。

移入窗口:加入当前站汽油,减去到下一站消耗汽油 
移出窗口:如果汽油不够了,去掉起始站再判断。
更新答案:走的站数量为一圈的数量时,返回答案

代码如下:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        int sum = 0;  //sum大于0表示能到下一站
        gas.insert(gas.end(), gas.begin(), gas.end());//倍增处理循环
        cost.insert(cost.end(), cost.begin(), cost.end());
        for (int i = 0, j = 0; i < 2 * n; i++) {
            sum += gas[i] - cost[i];//移入窗口
            while (j <= i && sum < 0) {//移出窗口
                sum -= gas[j] - cost[j];//绕不了一圈,把起始减去
                j++;
            }
            if (i - j + 1 == n) return j;//如果长度为n则绕一圈了
            //更新答案,直接返回
        }
        return -1;
    }
};

这个题有一个循环要求,我们可以倍增来解决

1234. 替换子串得到平衡字符串

我们想一下是要替换字符串,也就是窗口内字符串是不要的,那它有什么性质吗?能判断如何移出移入吗?简单想一下发现比较困难,所以我们转换思路,看看窗口外有什么性质。

我们发现如果要替换,则一定有字符长度大于l/4,而要替换的字符串一定包含这些,并只能大于等于它,那么外面的每种字符数量一定小于等于l/4,那么这时窗口内字符替换后一定能令整个字符串符合要求。

移入窗口:当前字符数量减1
移出窗口:窗口外字符都小于等于l/4时,缩小窗口看看还行不行,把左端字符加上
更新答案:窗口外字符都小于等于l/4时,统计答案,取min

代码如下:

class Solution {
public:
    int balancedString(string s) {
        int l = s.size();
        int n = l / 4;  //算1/4长度 
        unordered_map<char, int> m;//每种字符数量
        int ant = l;//答案,初始化为最大值
        for (char i : s)
            m[i]++;//遍历,统计一下字符串每个字符有多少个
        if (c['Q'] == n && c['W'] == n && c['E'] == n && c['R'] == n)
            return 0;//如果已经满足了直接返回
        for (int i = 0, j = 0; i < l; i++) {
            --c[s[i]];//移入窗口,窗口外字符--
            while (j <= i && c['Q'] <= n && c['W'] <= n && c['E'] <= n && c['R'] <= n) {//窗口外字符少于l/4
                ant = min(ant, i - j + 1);//更新答案
                ++c[s[j]];//移出窗口,窗口外字符++
                j++;
            }
        }
        return ant;
    }
};

这道题我们需要考虑窗口外的性质,这又是一种处理方式。

map有时可以用普通数组替换,但这里为了让大家更好掌握滑动窗口的套路,我则都采用了map

992. K 个不同整数的子数组

这道题看起来没什么思路,如何处理k,我们想一下如果用滑窗处理k的话不成立,因为是否移出窗口不好处理,如果当前窗口大于k我们移出左端直到等于k,但这是再移出左端也有可能还等于k,如果又移动,则刚才的结果丢了,所以比较麻烦,但是如果我们算当前个数小于等于k是没问题的,如果窗口长为n,则答案可以加上n,表示包含最右端的子数组个数有K个。

我们只需算两遍,小于等于k与小于等于k-1的答案再做差即可。

移入窗口:当前数字放入窗口
移出窗口:如果种类多了,则移出左端
更新答案:加上以该元素为尾元素的符合要求的数组的数量

代码如下:

class Solution {
public:
    int subarraysWithKDistinct(vector<int>& nums, int k) {
        return f(nums, k) - f(nums, k - 1);//做差
    }
    int f(vector<int>& nums, int k) {
        unordered_map<int, int> m;//每种数字有多少个
        int n = nums.size();
        int ant = 0;//答案
        for (int i = 0, j = 0; i < n; i++) {
            m[nums[i]]++;//移入窗口
            while (j <= i && m.size() > k) {//数目多了减少
                if (--m[nums[j]] == 0) //移出窗口
                    m.erase(nums[j]);
                j++;
            }
            ant += i - j + 1;//以i为最后元素的子数组有多少个
        }
        return ant;
    }
};

这道题思路较为巧妙,也是解题的难点

395. 至少有 K 个重复字符的最长子串

这道题也不好想思路,但是我们发现只有小写字母,也就是26个,那么我们可以循环遍历,当子串只有1,2,3......26种字符时,有无符合题意得子串。

移入窗口:当前字符数量加1
移出窗口:当字符种类超过当前循环要求时,移出左端
更新答案:检查窗口内每个字符是否符合要求,符合就更新为更长得长度

代码如下:

class Solution {
public:
    int longestSubstring(string s, int k) {
        unordered_map<char, int> m;//统计每种字符数量
        int n = s.size();
        int ant = 0;//答案
        for (int num = 1; num <= 26; num++) {
            m.clear();//清空,当窗口内有num种字符
            for (int i = 0, j = 0; i < n; i++) {
                m[s[i]]++;//移入窗口
                while (j <= i && m.size() > num) {
                    if (--m[s[j]] == 0)//移出窗口
                        m.erase(s[j]);
                    j++;
                }
                if ( check(m, k) )//符合要求,更新答案
                    ant = max(ant, i - j + 1);
            }
        }
        return ant;
    }
    bool check(unordered_map<char, int>& m, int k) {
        for (auto i = m.begin(); i != m.end(); i++)
            if (i->second < k)//遍历,看看每种字符数量是否符合题意
                return 0;
        return 1;
    }
};

这题难点在于思想,没有只用一次遍历得出答案,而是多次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值