滑动窗口题目

最近刷了两道非常经典的滑动窗口题目。感觉对自己帮助非常大,所以写下这篇博客来详细解释一下这两道题目,同时验证自己是否完全理解这两道题目。

题目一:找到字符串中所有字母异位词

题目链接:​​https://leetcode.cn/problems/find-all-anagrams-in-a-string/​

这道题目的要求很简单,即要求我们在s串中找到一个字串,这个子串具有一个特点那就是这个子串中的所有字母都是由p中的字母组成的。即子串中的字母数量和种类和p是一样的。

解法一:暴力枚举+哈希表

解法一的思路很简单我们可以枚举出s的所有字串然后从字串中寻找符合条件的字串然后,将值填入。

我们接下来来优化一下这个思路,首先思考一下当right移动到之后还有必要返回到left位置开始重新枚举吗?

以一个普通的字符串为例子

解法二:滑动窗口+哈希表

class Solution {
public:
    bool issame(int* hash, string& p, int* hash2)
    {
        for (int i = 0; i < p.size(); i++)
        {
            if (hash[p[i] - 'a'] != hash2[p[i] - 'a'])
            {
                return false;
            }
        }
        return true;
    }//这里使用了一个函数遍历完一遍哈希表确定两个哈希表是否相等
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        int hash[26] = { 0 };//作为s的哈希表
        int hash2[26] = { 0 };//作为p的哈希表
        if (p.size() > s.size())
        {
            return ret;
        }
        int left = 0;
        int right = 0;
        for (int i = 0; i < p.size(); i++)
        {
            hash2[p[i] - 'a']++;
        }
        //进窗口
        while (right < p.size())
        {
            hash[s[right] - 'a']++;
            right++;
        }
        while (right <= s.size())//这里不能将等号删除,否则会少记录一种情况,例如abab 和ab
            //当right将b放到哈希表中后right++等于了size(),不会进入循环记录最后一个答案
            //所以需要将等号加上
        {
            //记录答案
            if (issame(hash, p, hash2))
            {
                ret.push_back(left);
            }
            // 再次进窗口
            if (right < s.size()) {//这里需要判断一下right是否越界了,否则在right为s.size()的时候还是会进入hash表导致错误
                hash[s[right] - 'a']++;
            }
            //出窗口
            hash[s[left++] - 'a']--;
            right++;
        }
        return ret;
    }
};

这个代码还可以继续去优化,优化的点就在于于哈希表的比较,在上面的代码中我们是遍历完两个哈希表来比较哈希表是否相等的,这样就导致了每一次的检查都要遍历一遍哈希表,导致代码效率变低。

为了解决这个问题,可以使用一个count来计算符合条件的字符数量有好多。当count等于了p.size(),就可以更新答案。

下面是图解

下面是代码实现:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;//返回的答案
        //可以进行优化,使用count来统计哈希表中的有效字符的个数
        int hash1[26] = { 0 };//记录p中字母出现的次数
        int hash2[26] = { 0 };//记录滑动窗口中字母出现的次数
        if (p.size() > s.size())
        {
            return ret;
        }//特殊情况
        for (int i = 0; i < p.size(); i++)
        {
            hash1[p[i] - 'a']++;
        }
        int left = 0;
        int right = 0;
        int count = 0;//记录当前窗口中有效字母的个数
        while (right < s.size())
        {
            //进窗口
            hash2[s[right] - 'a']++;
            if (hash2[s[right] - 'a'] <= hash1[s[right] - 'a'])//如果此时进入的这个字符的个数是小于哈希表1中
                //这个字母出现的次数的,代表这是一个有效字母所以让count++
            {
                count++;
            }
            //判断出窗口
            while (right - left + 1 > p.size())
            {
                if (hash2[s[left] - 'a'] <= hash1[s[left] - 'a'])
                {
                    count--;
                }//这里代表出的是一个有效的元素
                hash2[s[left++] - 'a']--;
            }
            if (count == p.size())
            {
                ret.push_back(left);
            }
            right++;
        }
        return ret;
    }
};

题目二:串联所有单词的子串

题目链接:​​30. 串联所有单词的子串 - 力扣(LeetCode)​

这道题目有一个很重要的条件那就是words中每一个string的字符数是相等的。由此就能够知道出窗口的条件。

首先我们来看题目的求,在示例一中,在s字符串中bar foo这一段子串恰好完全就是由words中的两个字符串构成的,所以在s中的barfoo就是一个答案,将b的下标放到答案vector中。再然后便是在s字符串中的foobar这一段也是由words中的foo bar构成的所以最后将f的下标放到答案vector中。需要注意的是如果s字符串中出现了baroof这样的字符串是不符合答案要求的,因为oof并不是words数组中的string

下面是解法:

解法:

这道题目的暴力解法和上面那道题可以说是一摸一样的,因为如果将foo,看作字母a,bar看作字母b。然后将s中的字符每三个一组当作一个字符,那么这道题目就变得和上面一摸一样了。那么这道题也就可以使用滑动窗口和count优化了

代码:

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        //这道题目的思路为使用滑动窗口,首先将words中的每一个字符串的长度记录下来,然后在s中使用滑动窗口,来寻找满足条件的子串
        //将words中的字符串当作一个字符那么这道题目的思路也就是第438道题的思路
        unordered_map<string,int> hash1;//储存words中的字符串出现次数
        vector<int> ret;//用于储存答案
        int len = words[0].size();//得到单个字符串的长度
        int m = words.size();//有效字符串应该达到的次数
        int sum_len = len*m;
        for(int i = 0;i<m;i++)
        {
            hash1[words[i]]++;
        }
        for(int i = 0;i<len;i++)//滑动窗口进行的次数
        {
            int left = i;
            int right = i;//创建left和right指针
            unordered_map<string,int>hash2;//统计滑动窗口中的字符串出现次数
            int count = 0;
            while(right+len<=s.size())//开始滑动窗口
            {
                //进窗口
                string intmp = s.substr(right,len);//将字符串s从right位置开始的len个位置的字符拿出来
                hash2[intmp]++;
                if(hash1[intmp]&&hash2[intmp]<=hash1[intmp])
                //这里进行了一次优化,因为如果intmp在hash1中没有出现那么就没有必要去比较
                //并且对于map而言如果这个元素在之前没有出现过,而这里选要它出现的次数则会将这个字符串先放到map中去
                {
                    count++;
                }//当hash中存在这个字符串,并且hash2中这个字符串出现的次数小于等于hash1中出现的次数那就让count++
                //代表一个有效的字符串出现了
                //判断
                while(right -left+1>sum_len)
                {
                    string outtmp = s.substr(left,len);
                    if(hash1[outtmp]&&hash2[outtmp]<=hash1[outtmp])
                    {
                        count--;
                    }//代表一个有效的字符串被移除了
                    hash2[outtmp]--;
                    left+=len;
                }
                //更新答案
                if(m == count)
                {
                    ret.push_back(left);
                }
                right+=len;
            }            
        }
        return ret;
    }
};

题目三:最小覆盖子串

题目链接:​​LCR 017. 最小覆盖子串 - 力扣(LeetCode)​

首先来分析题目的要求结合示例题目

这道题目的暴力解法:暴力枚举+哈希表。将每一种枚举出来的子串放到哈希表中去,然后和t比较如果所有有效字母的数量是相等或大于的代表这是一个符合条件的子串,然后记录长度。下面我们来从暴力枚举的思路上检查是否存在可以优化的地方

然后还有一个优化,也就是关于哈希表比较的优化这里我们为了避免每一次比较都要遍历完整一遍的哈希表,这里就还是采用一个count来记录有效字母的种类,需要注意这里不是有效字母的数量,而是种类。因为题目的要求是t中的每一种字母都出现在了子串中才是符合条件的子串,如果t中每一个字母多次出现在了s中count是不能++的。不然就会出现错误。

故在滑动窗口中某一个有效字符出现的次数等于了在t中出现的次数时,代表这是一种有效的字符被增加到了窗口中count可以++,而在出窗口的时候,假设这个要出窗口的元素在窗口中出现的次数等于在t中出现的次数,那么删除这个元素代表的就是一个有效元素的删除,所以让count--。


class Solution {
public:
    string minWindow(string s, string t) {
        if (t.size() > s.size())
        {
            return "";
        }
        unordered_map<char, int>hash1;//统计t中字符出现的次数
        unordered_map<char, int>hash2;//统计滑动窗口中字符出现的次数
        int count = 0;//统计有效字符出现的种类
        int left = 0;
        string ret = s;
        int flag = 0;//默认为没有修改
        int right = 0;
        for (int i = 0; i < t.size(); i++)
        {
            hash1[t[i]]++;
        }
        int len = hash1.size();
        while (right < s.size())
        {
            //进窗口
            hash2[s[right]]++;
            if (hash1[s[right]] && hash2[s[right]] == hash1[s[right]])
            {
                count++;//当这个字符出现的次数在进入到窗口中后,
                //窗口中出现的字符串刚好等于haxi1中这个字符出现的次数
                //代表这是一个有效的字符种类
            }
            while (count == len)
            {
                string tmp(s.begin() + left, s.begin() + right + 1);
                if (tmp.size() <= ret.size())
                {
                    ret = tmp;
                    flag = 1;
                }
                if (hash1[s[left]] && hash1[s[left]] == hash2[s[left]])
                {
                    count--;
                }//当我们在没有删除left对应的这个字符的时候,这个字符的出现次数刚好等于这个字符在hash1中出现的次数
                //那么在删除完left之后代表的便是有效的字符种类少了一种
                hash2[s[left]]--;
                left++;
            }
            right++;
        }
        if (flag == 0)
        {
            return "";
        }
        else
        {
            return ret;
        }
    }
};

这些滑动窗口题目的代码其实并不是很难写,重要的是我们要知道什么时候使用滑动窗口,即滑动窗口的思想。因为我的水平优先,所以如果阅读的时候发现了错误,欢迎指出

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值