关于滑动窗口的那些事~

所谓的滑动窗口说白了就是找一个窗口,让它不断滑动,然后更新你想要的答案,大致思路如下:

int left = 0,right = 0;
while (right < s.size())
{
  //增大窗口
  windows.add(s[right]);
  right++;
  while (窗口需要缩小)
  {
    //缩小窗口
    windows.remove[s[left]];
    left++;
  }
}

不难看出来,滑动窗口算法的时间复杂度就是O(N),反正对我这种爱用暴力解法的人,O(N)已经很高效了。🤦‍♂️

废话不多说,直接上题:
LeetCode 76:最小覆盖子串:给你两个字符串s和t,在s中找到包含t中全部字母的最短子串,如果不存在则返回-1。

在这里插入图片描述

如果这道题我们使用暴力解法,时间复杂度必是O(N)。
我们来看看滑动窗口的思路:

  1. 在字符串s上使用left,right两个指针,分别初始化为0,索引区间为左闭右开,我们称它为一个窗口。
  2. 我们让left不动,不断增加right指针扩大窗口,直到窗口中包含了t中所有的字符。
  3. 紧接着停止扩大右窗口,增加left指针,使得窗口不断缩小,直到窗口中的字符串不再包含t中的所有字符。注意,我们每增加一次left,就要更新一次窗口里面的数据
  4. 不断重复步骤2和3,直到right到达字符串s的末尾

基于上述4个步骤,我们需要两个计数器need和window,分别记录t中字符出现的次数和窗口中相应字符的出现次数。

在这里插入图片描述
增加窗口,直到窗口包含t中所有字符:

在这里插入图片描述

现在我们缩小窗口(增加left):

在这里插入图片描述

直到窗口中的字符不再符合字符串t的要求,left就不继续移动:

在这里插入图片描述

之后重复上述动作,先right,再left,直到right到达字符串s的末尾。

分析完思路后看看怎么写代码:

一开始就说了需要两个计数器need和window,所以我们需要初始化两个表(ps:这里也可以使用两个vector)


        unordered_map<char,int>need,window;
        for (char c : t)
            need[c]++;

开滑!

int left = 0,right = 0,vaild = 0;
while (right < s.size())
{
  //滑
}

这里面有个vaild变量表示的是窗口中满足need条件的字符个数,若vaild==need.size(),表示窗口已经完全覆盖了字符串t。

可以思考以下几个问题:

  1. right扩大窗口的时候,加入了字符,应该更新哪些数据?
  2. 什么条件的时候,应该停止扩大窗口
  3. 缩小窗口的时候,应该更新哪些数据
  4. 我们需要的结果是在扩大窗口的时候更新数据还是在缩小窗口的时候更新数据

经过以上的分析,很显然,在字符进入窗口时,window计数器应该增加,在字符离开窗口时,window计数器应该减少。当valid与need.size()相等的时候,收缩窗口,更新最终结果

完整代码:

using namespace std;
class Solution {
public:
    string minWindow(string s, string t)
    {
        unordered_map <char,int>need,window;
        for (char c : t)
            need[c]++;
        int left = 0,right = 0,valid = 0;
        int len = INT_MAX,start = 0;
        while (right < s.size())
        {
            //开始滑动窗口
            char c = s[right];
            right++;
            if (need.count(c))
            {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }
            //判断左侧窗口是否要收缩
            while (valid == need.size())
            {
                if (right - left < len)
                {
                    start = left;
                    len = right - left;
                }
                //字符d即将移除窗口
                char d = s[left];
                //收缩
                left++;
                if (need.count(d))
                {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        return len == INT_MAX ? "" : s.substr(start,len);
    }
};

//

有了基本的滑动窗口的思路:下面几道题稍加修改就能解决啦

LeetCode 567:字符串的排列

给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。

换句话说,第一个字符串的排列之一是第二个字符串的 子串 。

在这里插入图片描述
这道题大体思路与第一题相同,只不过在判断什么时候需要缩小窗口时需要注意一下,当我们窗口的大小>=s1.size()的时候,就开始缩小窗口并且需要判断vaild是否等于need.size(),如果vaild==need.size(),直接返回true。

完整代码:

//剑指 Offer 38. 字符串的排列

#include <unordered_map>
#include <string>
using namespace std;
class Solution {
public:
    bool checkInclusion(string s1, string s2)
    {
        unordered_map<char,int>need,window;
        for (char c : s1)
            need[c]++;
        int left = 0,right = 0,valid = 0;
        while (right < s2.size())
        {
            //扩大窗口
            char c = s2[right];
            right++;
            if (need.count(c))
            {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }
            //判断是否需要缩小窗口
            while (right - left >= s1.size())
            {
                //在这里判断是否找到了合法的字符串
                if (valid == need.size())
                    return true;
                char d = s2[left];
                //字符d移除窗口
                left++;
                if (need.count(d))
                {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        //没找到
        return false;
    }
};

//

LeetCode 438:找到字符中所有字母的异位词

如果理解了上面2道题,这道题自然不在话下:

class Solution {
public:
    vector<int> findAnagrams(string s, string p)
    {
        unordered_map<char,int>need,window;
        vector<int>res;
        int left = 0,right = 0,valid = 0;
        for (char c : p)
            need[c]++;
        while (right < s.size())
        {
            //扩大窗口
            char c = s[right];
            right++;
            if (need.count(c))
            {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }
            while (right - left >= p.size())
            {
                //缩小窗口,并收集结果
                if (valid == need.size())
                    res.push_back(left);
                char d = s[left];
                left++;
                if (need.count(d))
                {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        return res;
    }
};

//

LeetCode 3:无重复字符的最长子串

这个题比较有意思的就是,它不需要need和vaild,更新窗口内的数据只需要判断window计数器即可,当window[c] > 1的时候,就可以缩小窗口,最后将窗口长度最大值求出即可

完整代码:

class Solution {
public:
    int lengthOfLongestSubstring(string s)
    {
        unordered_map<char,int>window;
        int left = 0,right = 0;
        int res = 0;//保存结果
        while (right < s.size())
        {
            //扩大窗口
            char c = s[right];
            right++;
            window[c]++;
            //判断是否需缩小窗口
            while (window[c] > 1)//当window[c] > 1:说明有窗口内有重复字符
            {
                char d = s[left];
                left++;
                window[d]--;
            }
            res = max(res,right - left);
        }
        return res;
    }
};

其实滑动窗口这种思路是双指针技巧中比较难的技巧,不过多画画图,其实这个技巧还是非常有用的。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值