【基础算法总结】滑动窗口二

发布于:2024-05-07 ⋅ 阅读:(21) ⋅ 点赞:(0)

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.水果成篮

题目链接904. 水果成篮

题目分析

在这里插入图片描述

这道题描述很多,主要意思是从左往右有一排树,给你两个篮子,每个篮子只能装一种类型水果。你可以任选从某一颗数开始摘水果,并且一棵树必须要摘一个水果。也就是说不能跳着摘水果。当你走到某棵树面前,但树上水果与你两个篮子水果类型不一样,就不能在继续了。下面就是找的两种情况。
在这里插入图片描述
根据上面的理解,这道题我们可以转化一下:
找出一个最长的子数组的长度,并且子数组中不超过两种类型的水果

算法原理

首先要想到暴力求解,两层for循环,但是这里要求一个子数组中水果不能超过两种类型,对于这种不能有重复的元素或者这道题的意思,我们可以使用哈希表,因此第一种解法出来了。

解法一:暴力求解+哈希表
定义两个指针left=0,right=0,当right走到第三种水果位置,kinds>2了,此时我们会让left往前走,然后right回溯,在重新往前走。这就是我们的想法。
在这里插入图片描述
接下来我们看看有没有优化的可能。
这里我们就不在以特例来进行讲解,因为特殊例子并不具有代表性,可能你的想法值适用于这一个例子。如果随便拿一个数组都能满足,才是正确解法!

当right此时在往前走一个位置,kind=3,就不满足情况,因此我们现在让left往前走一步。现在我们讨论有没有必要让right回溯。left往前走只有两种情况,
一 :kinds 减小 right往前走
二 :kinds 不变 right不变

因此right根本不用回溯,要么不变,要么往前走。
这不就是同向双指针!这不是就是滑动窗口的思想吗!

在这里插入图片描述
上面的分析尤为重要,滑动窗口就那几步代码很简单,但是难的是你想到使用滑动窗口,这个why?很重要!

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

  1. left=0,right=0
  2. 进窗口
  3. 判断
    出窗口
  4. 更新结果

当你知道用滑动窗口了,就可以使用特例来看看怎么写代码。

当出窗口一定要注意,因为进入这个hash的这种类型的树可不止一颗,如果我们用的是这种hash只统计类型,那出窗口减去这种类型树的时候时候,明明数组里面还有这种类型的树,但是一下就减没了,是有问题的,因此hash里面不光有这种类型,还要有这里类型树的个数。

进窗口然后判断,当判断后窗口里面一定是符号条件的,然后再更新结果。
在这里插入图片描述

class Solution {
public:
    int totalFruit(vector<int>& fruits) {

        unordered_map<int,int> mp;
        int len=0;
        for(int left=0,right=0;right<fruits.size();++right)
        {
        	
            mp[fruits[right]]++;//进窗口
            while(mp.size()>2)//判断
            {
            	//出窗口
                mp[fruits[left]]--;
                if (mp[fruits[left]] == 0)
                    mp.erase(fruits[left]);
                ++left; 
            }
            //更新结果
            len=max(len,right-left+1);
        }
        return len;

    }
};

虽然代码可以通过,时间有点久,这是因为我们使用了容器导致的,对于数据范围有限,可以不使用容器,这样会提高时间效率。

在这里插入图片描述

class Solution {
public:
    int totalFruit(vector<int>& fruits) {

        //unordered_map<int,int> mp;
        int hash[100001] = {0};
        int len=0,kinds=0;
        for(int left=0,right=0;right<fruits.size();++right)
        {
            if(hash[fruits[right]] == 0)
                kinds++;
            //进窗口 
            hash[fruits[right]]++;
            //判断
            while(kinds>2)
            {
                //出窗口
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0)
                    --kinds;
                ++left; 
            }
            //更新结果
            len=max(len,right-left+1);
        }
        return len;

    }
};

2.找到字符串中所有字母异位词

题目链接438. 找到字符串中所有字母异位词

题目分析

在这里插入图片描述

异位词:相同字母重新排序形成的字符串
在这里插入图片描述
在解决这道题前面,我们首先想如何快速判断两个字符串是否是 “异位词”
在这里插入图片描述
首先肯定会想把两个字符串排序一下,然后再每个字依次对比。这样是肯定能解决问题的,时间复杂度O(n+nlog^n)

还有其他更好的方法吗?
以前说过对于找重复的元素,什么最快?
肯定是哈希表
因此我们直接用哈希表就可以了。
在这里插入图片描述

算法原理

解法一:暴力求解+哈希表
我们可以把所有字符串中所有字串都找到结合哈希表,肯定能得到正确结果,但是肯定通过不了,时间复杂度太高了。
在这里插入图片描述

接下来想想有没有优化的可能
定义left,right指针,当right走到>len的位置,就停下来,因为超过p的长度了,根本就不可能是异位词了。然后left往前走一步,注意每次left只用走一步就可以了。因为此时left到right区间长度刚好等于len,所有left每次就走一步。
在这里插入图片描述

然后right也不用回溯,因为元素就已经在hash1中。并且right每次也就往后走一步就行了。走多了就不满足了。
在这里插入图片描述

到这里我们肯定直到是要用滑动窗口了,但是这里的滑动窗口和之前的不一样。

滑动窗口的分类:

1.非固定大小滑动窗口
利用单调性,规律,每次进窗口出窗口可能会好多次。判断出窗口是一个循环。

2.固定大小滑动窗口
满足特定条件,每次进窗口出窗口一次。判断出窗口就执行一次。

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

在这里插入图片描述

class Solution {
public:
    bool check(int* hs,int* hp)
    {
        for(int i=0;i<26;++i)
        {
            if(hs[i] != hp[i])
                return false;
        }
        return true;
    }

    vector<int> findAnagrams(string s, string p) 
    {
         vector<int> ret;
         int hashs[26]={0},hashp[26]={0};
         for(auto& ch:p)
             hashp[ch-'a']++;

         int len=p.size();
         for(int left=0,right=0;right<s.size();++right)
         {
             hashs[s[right]-'a']++;
             if(right-left+1>len)
             {
                 hashs[s[left]-'a']--;
                 ++left;
             }

             if(check(hashs,hashp))
                 ret.push_back(left);

         }
         return ret;
     }
}

这道题给的是小写字母,因此hash大小设为26。然后每次判断后都要check内部循环检查一下两个哈希表是否相等,因此这个时间复杂度是O(26*N)最终是O(N),对于这道题来说没问题,但是给你换成字符串就有问题了。

所以在优化一下:
优化:更新结果的判断条件
利用变量 count 来统计窗口中 “有效字符” 的个数

目前p中字符共3个,其中字符a、b、c分别占一个。那如果保证滑动窗口内有效字符(a、b、c)个数等于len,并且与p中字符a、b、c个数一一对应。那就能直接说明该滑动窗口是满足条件的,此时只要把left下标添加道返回结果就行了。这样就不用在每次循环判断两个hash表了。
在这里插入图片描述
当right第一次指向c,count+1,但是当right往后走再次指向c,就不要跟新count了,因为p中c字符就才1个!
在这里插入图片描述
当right到b的时候,count+1。此时判断一下,有效字符count个数为2,len=3,不相等。如果count个数正好等于3说明窗口里面全都是有效字符。
所以我们可以做到一边在滑动窗口里面添加元素,一边维护count,同时我们还可以知道什么时候是字母异位词。
在这里插入图片描述
然后right再走遇到a,a变成1,然后更新count+1。但是此时这个窗口大小已经超过len,因此先要出窗口left往后移一步。滑动窗口内字符c个数要减1,但是删掉之前count需不需要更新呢?并不需要,因为c=2,比p中c=1多一个字符,那删掉的c就是多余字符,因此并不需要更新count。在判断一下此时有效字符count==len,说明窗口里是合法字符,此时把left添加到返回数组就可以了。

在这里插入图片描述
总结:
进窗口: 进窗口后,hash2[in] <= hash1[in] —> count++
出窗口: 出窗口前,has2[out] <= hash1[out] —> count–
更新结果:count==len

class Solution {
public:

    vector<int> findAnagrams(string s, string p) {

        vector<int> ret;
        int hash1[26]={0};//统计字符串 p 中每个字符出现的个数

        for(auto& ch:p) hash1[ch-'a']++;

        int hash2[26]={0};//统计窗口里面的每一个字符出现的个数

        int len=p.size(),count=0;
        for(int left=0,right=0;right<s.size();++right)
        {
            char in=s[right];
            //进窗口 + 维护 count
            if(++hash2[in -'a'] <= hash1[in -'a']) 
                count++;

            if(right-left+1>len)//判断
            {
                //出窗口 + 维护 count
                char out=s[left++];
                if(hash2[out -'a']-- <= hash1[out-'a'])
                    count--;

            }

            if(count == len)
                ret.push_back(left);

        }
        return ret;
    }
}      

3.串联所有单词的子串

题目链接30. 串联所有单词的子串
题目分析

在这里插入图片描述

注意这道题很重要一点就是 words 中所有字符串 长度相同
其次这道题的意思是让在s字符串中,找到words数组中字符串的任意组合。找到后返回下标。

在这里插入图片描述

算法原理
其实这道题和上面的 438. 找到字符串中所有字母异位词 ,解法思路一模一样,为什么这样说呢。因为上面这道题让找的是字母,这道题增加难道让找的是字符串的异位串!

words 中所有字符串 长度相同,我们设它为len长,然后把s中len个字符组合在一起,这不就是438. 找到字符串中所有字母异位词的题吗!

在这里插入图片描述

解法一:暴力求解+哈希表

不过有三处地方和上面不一样,需要注意,其他方面都是一样的解题思路

  1. 因为这里是字符串,因此我们使用unordered_map<string,int>容器,string用来记录字符串,int用来记录每个字符串出现的频次
  2. left 与 right 指针的移动
    left 和 right每次移动的步长,是单词的长度 ----> len
    在这里插入图片描述
  3. 因为我们指针是跳着走的,我们有的情况没有找完,因此滑动窗口执行的次数要len次

在这里插入图片描述

其他思路是完全一模一样的。

class Solution {
public:

    vector<int> findSubstring(string s, vector<string> words) {

        vector<int> ret;

        unordered_map<string,int> hash1;//保存 words 里面所有字符串频次
        for (auto& str : words) hash1[str]++;

        int len=words[0].size(),m=words.size();
        for(int i=0;i<len;++i)//执行len次滑动窗口
        {
            unordered_map<string,int> hash2;//维护窗口内字符串
            //小心right往后走截取字符串越界问题!
            for(int left=i,right=i,count=0;right+len<=s.size();right+=len)
            {
                // 进窗口 + 维护 count
                string in=s.substr(right,len);
                hash2[in]++;
                if(hash1.count(in) && hash2[in] <= hash1[in]) 
                    ++count;

                //判断
                if(right-left+1>len*m)
                {
                    //出窗口 + 维护 count
                    string out=s.substr(left,len);
                    if(hash1.count(out) && hash2[out] <= hash1[out])
                        --count;
                    hash2[out]--;
                    left+=len;
                }
                //更新结果
                if(count == m)
                    ret.push_back(left);
            }
        }
        return ret;

4.最小覆盖子串

题目链接76. 最小覆盖子串

题目分析

在这里插入图片描述
本题是让在一个字符串找到包含另一个字符串的最短字串,其中我们看它的注意提醒,对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。这句话有点抽象,我们画图说明一下

注意看这个画圈的地方,这也是符合条件的字串,虽然B有两个字符。也就是说在s中找到符合要求的子字符串中对应字符可以大于的t字符串中的字符,这种字符串是满足条件的!

在这里插入图片描述

算法原理

我们首先会想到暴力枚举把所有符合条件的子字符串找出来,一定可以找到其中最小的。那怎么找到符合条件的子字符串呢?我们这里还是借助哈希表。

解法一:暴力枚举+哈希表
在这里插入图片描述
下面看看有没有优化的可能,我们优化时暂时不用特例,搞一个任何情况下都满足的情况,现在left和right区间就是满足条件的最短子字符串。暴力枚举就是left+1,right回溯,然后重新往后找。

在这里插入图片描述
现在看看right有没有必要回溯在重新找。当left+1,可能有两种情况:
1.字符串不符合要求了
2.字符串依然符合要求

字符串不符合要求了那就是变短了,right如果回溯然后重新走到老位置,不还是不符合要求吗?此时right不需要回溯,右移往后走就行了

字符串依然符合要求,那right回溯之后还是走到老位置不还是符合要求吗?此时right不动就行了

在这里插入图片描述
两个指针,同向! -----> 滑动窗口!

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

滑动窗口就那几步,我们这时可以使用特例来确实什么时候进窗口,判断,出窗口,更新结果

在这里插入图片描述
每次判断都要遍历数组,太麻烦了。我们把判断优化一下。
优化: 判断条件
使用变量 count 标记有效字符的种类
如果还像之前统计有效字符个数就有问题了,虽然有效字符个数相等但是并不是字串!
在这里插入图片描述

优化:
一定是字符个数相等才能说是种类一样!

进窗口后当 hash2[in] == hash1[in] count++

出窗口前当 hash2[out] == hash1[out] count- -

判断 count == hash1.size() 滑动窗口内字符种类和hash表中字符种类相等

class Solution {
public:

    string minWindow(string s, string t) {
        int hash1[128]={0};// 统计字符串 t 中每一个字符出现的频次
        int kinds=0;// 统计有效字符有多少种
        for(auto& ch : t) 
        {
            if(hash1[ch]++ == 0)
                ++kinds;
        }        

        int hash2[128]={0};// 统计窗口内每个字符的频次
        int begin=-1,minlen=INT_MAX;
        for(int left=0,right=0,count=0;right<s.size();++right)
        {
            //进窗口 + 维护count
            char in=s[right];
            if(++hash2[in] == hash1[in])
                ++count;
            
            //判断
            while(count == kinds)
            {
                //更新结果
                if(right-left+1 < minlen)
                {
                    minlen=right-left+1;
                    begin=left;
                }

                //出窗口 + 维护 count
                char out=s[left++]; 
                if(hash2[out]-- == hash1[out])
                    --count;

            }
        }

        if(begin == -1) return "";
        else return s.substr(begin,minlen);

    }
};