关于滑动窗口的那些事~

Posted 一棵灬胡杨树

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于滑动窗口的那些事~相关的知识,希望对你有一定的参考价值。

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

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;
    }
};

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

以上是关于关于滑动窗口的那些事~的主要内容,如果未能解决你的问题,请参考以下文章

app 性能优化的那些事

转载关于烂代码的那些事

关于代码调试de那些事

关于烂代码的那些事(下)

关于泛型那些事?

关于Android架构那些事