• 算法之滑动窗口


    题目1:209. 长度最小的子数组 - 力扣(LeetCode) 

     解法⼀(暴力求解):

    思路:
    从前往后, 枚举数组中的任意⼀个元素, 把它当成起始位置, 然后从这个起始位置开始, 然
    后寻找⼀段最短的区间, 使得这段区间的和「⼤于等于」⽬标值.
    将所有元素作为起始位置所得的结果中, 找到最⼩值即可。

    解法⼆(滑动窗⼝):

    思路:

    由于此问题分析的对象是⼀段连续的区间, 因此可以考虑滑动窗⼝的思想来解决这道题.
    这道题我们不妨换一个角度思考, 我们先不去思考怎么去针对这个最短区间做文章, 而是讨论每个下标i作为窗口的左端的情况下能取得的最短窗口长度, 在窗口缩短的过程中不断地更新这个最短长度, 最后的长度就是最短长度.

    那怎么去求得这个最短长度呢?

     1. 将右端元素划⼊窗⼝中(right++), 统计出此时窗⼝内元素的和
    如果窗⼝内元素之和sum⼤于等于 target : 更新长度len, 并且将左端元素划出去(left++), 更新sum, 为什么可以直接把最左端元素划出窗口?

    因为此时窗口内的sum已经是以left为起始的最短满足条件的子数组了, 算上right之后的数窗口只会更长, 所以left为起点的这一组就判断完了.

    2. left划出窗口后, 如果当前窗口的sum比target小, 肯定right要继续++, 但是当前窗口的sum也可能大于等于target, 所以应该继续判断sum去决定right是不是要继续++, 那此时是应该继续划出窗口呢? 还是right回退到left重新算一遍窗口内的sum呢? 

    right不需要再回退到left重新来算一遍sum了, 抛开right对应的值, 此时窗口内的和肯定比上一个窗口小, 所以当前窗口以left为起点的子窗口的sum肯定更小,  所以right在上次的位置待着即可.

    此时区间内的和要么是小于target的, 继续right++; 要么是大于等于target的, 也就是当前最小的一种子数组, 继续重复之前的步骤即可

    可以注意到过程中left和right都是同向移动的, 这样的同向双指针称为滑动窗口: 

    为何滑动窗⼝可以解决问题, 并且时间复杂度更低?

    ▪ 这个窗⼝寻找的是: 以当前窗⼝最左侧元素(记为left1 )为基准, 符合条件的情况.也就是在这道题中, 从left1开始, 满⾜区间和sum >= target 时的最右侧(记为right1 )能到哪⾥.
    ▪ 我们既然已经找到从 left1 开始的最优的区间, 那么就可以⼤胆舍去left1,但是如果继续像⽅法⼀⼀样,重新开始统计第⼆个元素(left2)往后的和, 势必会有⼤量重复的计算(因为我们在求第⼀段区间的时候, 已经算出很多元素的和了, 这些和是可以在计算下次区间和的时候⽤上的).
    ▪ 此时, rigth1 的作⽤就体现出来了, 我们只需将 left1 这个值从 sum 中剔除, 从right1这个元素开始, 往后找满⾜left2元素的区间(此时right1也有可能是满⾜的, 因为left1可能很⼩, sum 剔除掉left1之后, 依旧满⾜⼤于等于target ), 这样我们就能省掉⼤量重复的计算。
    ▪ 这样我们不仅能解决问题, ⽽且效率也会⼤⼤提升。


    时间复杂度:虽然代码是两层循环,但是left指针和 right指针都是不回退的, 两者最多都往后移动n次, 因此时间复杂度是? O(N)  

    1. class Solution {
    2. public:
    3. int minSubArrayLen(int target, vector<int>& nums)
    4. {
    5. int left = 0, right = 0, n = nums.size();
    6. int sum = 0, len = INT_MAX;
    7. while(right < n)
    8. {
    9. //先记录和
    10. sum += nums[right];
    11. while(sum >= target)
    12. {
    13. //更新最小长度
    14. int newlen = right - left + 1;
    15. len = len < newlen ? len : newlen;
    16. //出窗口
    17. sum -= nums[left++];
    18. }
    19. //移动滑动窗口
    20. right++;
    21. }
    22. //如果len还是INT_MAX就表示没找到这样的一个数组就返回0, 否则返回记录的最小长度
    23. return len == INT_MAX ? 0 : len;
    24. }
    25. };

    题目2:3. 无重复字符的最长子串 - 力扣(LeetCode) 

    解法⼀(暴⼒求解):

    思路:
    枚举「从每⼀个位置」开始往后, ⽆重复字符的⼦串可以到达什么位置, 找出其中⻓度最⼤的即可.在往后寻找⽆重复⼦串能到达的位置时, 可以利⽤「哈希表」统计出字符出现的频次, 来判断什么时候⼦串出现了重复元素.

    解法⼆(滑动窗⼝):

    思路:
    研究的对象依旧是⼀段连续的区间, 因此继续使⽤「滑动窗⼝」思想来优化, 
    让滑动窗⼝满⾜: 窗⼝内所有元素都是不重复的。
    做法:右端元素 ch 进⼊窗⼝的时候, 哈希表统计这个字符的频次.
    ▪ 如果这个字符出现的频次超过 1 , 说明窗⼝内有重复元素, 那么就从左侧开始划出窗⼝, 直到ch这个元素的频次变为1, 然后再更新结果, 因为left要跳过当前right指的重复元素, 才有机会得到新的最长子串, 否则每次都会被right卡死, 长度始终无法突破最开始的长度.
    ▪ 如果没有超过1, 说明当前窗⼝没有重复元素, 可以直接更新结果.

    1. class Solution {
    2. public:
    3. int lengthOfLongestSubstring(string s)
    4. {
    5. map<char,int> m;
    6. int left = 0,right =0;
    7. int len = 0;
    8. while(right < s.length())
    9. {
    10. m[s[right]]++;//计数
    11. while(m[s[right]] > 1)
    12. m[s[left++]]--;//左边一直滑动,直到跳过重复字符
    13. len = max(len,right - left + 1);//更新长度
    14. right++;//向右滑动窗口
    15. }
    16. return len;
    17. }
    18. };

    题目3:1004. 最大连续1的个数 III - 力扣(LeetCode)

    解法(滑动窗⼝):

    思路:
    不要去想怎么翻转, 这道题的结果⽆⾮就是⼀段连续的 1 中间塞了k 个0 , 因此, 我们可以把问题转化成:求数组中⼀段最⻓的连续区间, 要求这段区间内 0 的个数不超过 k 个.

    既然是连续区间, 可以考虑使⽤滑动窗⼝来解决问题.

    写法1: 

    1. class Solution {
    2. public:
    3. int longestOnes(vector<int>& nums, int k)
    4. {
    5. int left = 0, right = 0;
    6. int zerocount = 0;
    7. int len = 0;
    8. while(right < nums.size())
    9. {
    10. //进窗口
    11. while(right < nums.size() && (nums[right] == 1 || (nums[right] == 0 && zerocount < k)))
    12. {
    13. if(nums[right] == 0)
    14. zerocount++;
    15. right++;
    16. }
    17. //更新长度
    18. len = max(len,right - left);
    19. //出窗口
    20. while(left < nums.size() && zerocount == k)
    21. {
    22. if(nums[left] == 0)
    23. zerocount--;
    24. left++;
    25. }
    26. }
    27. return len;
    28. }
    29. };

     写法2:

    1. class Solution
    2. {
    3. public:
    4. int longestOnes(vector<int>& nums, int k)
    5. {
    6. int ret = 0;
    7. for(int left = 0, right = 0, zero = 0; right < nums.size(); right++)
    8. {
    9. if(nums[right] == 0) zero++; // 进窗⼝
    10. while(zero > k) // 判断
    11. if(nums[left++] == 0) zero--; // 出窗⼝
    12. ret = max(ret, right - left + 1); // 更新结果
    13. }
    14. return ret;
    15. }
    16. };

    题目4:1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode) 

    解法(滑动窗⼝):

    思路:
    题⽬要求的是数组「左端+右端」两段连续的和为 x 的最短数组, 信息量稍微多⼀些,不易理清
    思路, 正难则反, 我们可以转化成求数组内⼀段连续的和为sum(nums) - x的最⻓数组。此时, 就是滑动窗⼝问题. 

    1. class Solution {
    2. public:
    3. int minOperations(vector<int>& nums, int x)
    4. {
    5. int left = 0, right = 0;
    6. int n = nums.size();
    7. int sumi = 0, len = -1;
    8. int sum = 0;
    9. for(auto& e: nums)
    10. sum += e;
    11. while(right < n)
    12. {
    13. sumi += nums[right];
    14. while(left < n && sumi > sum - x)
    15. sumi -= nums[left++];
    16. if(sumi == sum - x)
    17. len = max(len, right-left+1);
    18. right++;
    19. }
    20. if(len == -1)
    21. return -1;
    22. return nums.size() - len;
    23. }
    24. };

    题目5:904. 水果成篮 - 力扣(LeetCode) 

    解法(滑动窗⼝):
    思路:
    研究的对象是⼀段连续的区间, 可以使⽤滑动窗⼝思想来解决问题, 让滑动窗⼝满⾜:窗⼝内⽔果的种类只有两种.

    做法: 右端⽔果进⼊窗⼝的时候, ⽤哈希表统计这个⽔果的频次, 这个⽔果进来后, 判断哈希表的
    大小:
    ▪ 如果大小超过2: 说明窗⼝内⽔果种类超过了两种, 先记录结果然后出窗口, 从左侧开始依次将⽔果划出窗口直到哈希表的大小小于等于2, 然后更新结果;
    ▪ 如果没有超过2, 说明当前窗⼝内⽔果的种类不超过两种, 继续统计.

     while循环:

    1. class Solution {
    2. public:
    3. int totalFruit(vector<int>& fruits)
    4. {
    5. unordered_map<int,int> m;
    6. int left = 0, right = 0, n = fruits.size(), ret = 0;
    7. while(right < n)
    8. {
    9. m[fruits[right++]]++;
    10. while(left < n && m.size() > 2)
    11. {
    12. if(--m[fruits[left]] == 0)
    13. m.erase(fruits[left]);
    14. left++;
    15. }
    16. ret = max(ret, right - left);
    17. }
    18. return ret;
    19. }
    20. };

     参考别人的for循环实现:

    1. class Solution {
    2. public:
    3. int totalFruit(vector<int>& fruits) {
    4. int hash[100001] = {0};
    5. int ret = 0;
    6. for(int left=0,right=0,kinds=0; rightsize();right++)
    7. {
    8. if(hash[fruits[right]] == 0) kinds++;//如果插入的水果个数为0,种类++
    9. hash[fruits[right]]++;
    10. //判断是否需要出窗口
    11. while(kinds > 2)
    12. {
    13. hash[fruits[left]]--;
    14. if(hash[fruits[left]] == 0)//如果kinds减为0种类--
    15. kinds--;
    16. left++;
    17. }
    18. ret = max(ret,right-left+1);
    19. }
    20. return ret;
    21. }
    22. };

    题目6:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

    判断异位词只需要创建两个字母的哈希表, 如果哈希表相同, 那么这两个串就是异位词, 对于这题来说, 可以考虑用哈希表是否相等来判断异位词, 大思路还是滑动窗口, 而且这是一个定长的滑动窗口, 同样还是分为入窗口, 判断, 出窗口, 更新结果这几个步骤:

    1. class Solution {
    2. public:
    3. bool isEqual(int* h1, int* h2)
    4. {
    5. for(int i = 0;i<26;i++)
    6. if(h1[i] != h2[i])
    7. return false;
    8. return true;
    9. }
    10. vector<int> findAnagrams(string s, string p) {
    11. vector<int> ret;
    12. int hash1[26] = {0};//统计s中的字符个数
    13. int hash2[26] = {0};//统计p中的字符个数
    14. for(auto& e : p)
    15. hash2[e-'a']++;
    16. int left = 0, right = 0, n = s.size(), m = p.size();
    17. while(right < n)
    18. {
    19. hash1[s[right]-'a']++;//入窗口
    20. if(right-left+1 > m)//判断
    21. {
    22. hash1[s[left++]-'a']--;//出窗口
    23. }
    24. if(isEqual(hash1,hash2))
    25. ret.push_back(left);
    26. right++;
    27. }
    28. return ret;
    29. }
    30. };

     但是判断哈希表是否相等的代价有点大了, 这题还好, 如果需要判断的是字符串等自定义类型的哈希表, 那代价就很大了, 所以考虑进行优化更新结果的判断条件:

    利用一个变量count来统计窗口中有效字符的个数, 我们全程只需要去移动滑动窗口的同时去维护这个count, 通过比较count是否和p的长度相等, 即可判断是否是字母异位词:

    如何去判断一个字符是否有效呢?

    如果 hash1[s[right]-'a'] <= hash2[s[right]-'a], 也就是一个字符的出现次数小于等于有效字符出现数量, 那这个字符是有效的字符.

    1. class Solution {
    2. public:
    3. vector<int> findAnagrams(string s, string p) {
    4. vector<int> ret;
    5. int hash1[26] = {0};
    6. int hash2[26] = {0};
    7. for(auto& e : p)
    8. hash2[e-'a']++;
    9. int left = 0, right = 0, count = 0, n = s.size(), m = p.size();
    10. while(right < n)
    11. {
    12. hash1[s[right]-'a']++;//进窗口
    13. if(hash1[s[right]-'a'] <= hash2[s[right]-'a'])//判断是否是有效字符
    14. count++;
    15. if(right-left+1 > m)//根据窗口长度判断出窗口
    16. {
    17. if(hash1[s[left]-'a']-- <= hash2[s[left]-'a'])//判断是否是有效字符
    18. count--;
    19. left++;
    20. }
    21. if(count == m)
    22. ret.push_back(left);
    23. right++;
    24. }
    25. return ret;
    26. }
    27. };

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

    此题和上一题几乎一模一样, 但区别有三点:

    1. 哈希表不同, 此题需要借助unordered_map

    2. 异位词变成了子串, 要统计字符串的频次

    3. 需要多次移动滑动窗口

    1. class Solution {
    2. public:
    3. vector<int> findSubstring(string s, vector& words)
    4. {
    5. unordered_mapint> hash1;
    6. unordered_mapint> hash2;//保存words里字符串出现的频次
    7. for(auto& e : words)
    8. hash2[e]++;
    9. vector<int> ret;//保存返回值
    10. int left = 0, right = 0;//维护滑动窗口
    11. int count = 0; //统计有效字符个数
    12. int ns =s.length(), nw = words.size(),len = words[0].length();//保存相关参数
    13. for(int i = 0; i//执行len次滑动窗口
    14. {
    15. //相关参数初始化
    16. right = i;
    17. left = i;
    18. hash1.clear();
    19. count = 0;
    20. while(right+len <= ns)
    21. {
    22. string in = s.substr(right,len);
    23. hash1[in]++;//进窗口
    24. //判断是否是有效字符
    25. if(hash1[in] <= hash2[in])
    26. count++;
    27. //出窗口+维护count+维护窗口
    28. if(right-left+1 > nw*len)
    29. {
    30. string out = s.substr(left,len);
    31. if(hash1[out]-- <= hash2[out])
    32. count--;
    33. left+=len;
    34. }
    35. //更新结果
    36. if(count == nw)
    37. ret.push_back(left);
    38. right+=len;
    39. }
    40. }
    41. return ret;
    42. }
    43. };

    小优化: 因为unordered__map调用operator[]的时候对于key不存在的元素会先插入key并给value赋值为0再返回, 有时间开销, 所以可以先提前判断hash2里有没有要查询的字符串, 如果没有就不需要去比较了, 肯定不是有效字符.


    题目8: 最小覆盖子串(hard)

    滑动窗口 + 哈希表:
    研究对象是连续的区间, 因此可以尝试使用滑动窗口的思想来解决, 如何判断当前窗口内的所有字符是符合要求的呢?

    我们可以使用两个哈希表, 其中一个将目标串的信息统计起来, 另一个哈希表动态的维护窗口内字符串的信息.
    动态哈希表中包含目标串中所有的字符, 并且对应目标串的字符的个数都不小于目标串的哈希表中各个字符的个数, 比如s="ABBC", t = "ABC", s也是t的一个覆盖子串.

    此题依然可以用一个count去统计有效字符的个数, 而不是直接去比较哈希表, 当 hash1[s[right]-'A'] <= hash2[s[right]-'A'] 的时候count++, 相应的hash1[s[left-'A'] <= hash2[s[left-'A']的时候count--, 和第六题思路类似:

    1. class Solution {
    2. public:
    3. string minWindow(string s, string t)
    4. {
    5. int hash1[128] = {0};
    6. int hash2[128] = {0};
    7. int kinds = 0;
    8. //统计次数并记录有效字符
    9. for(auto& e: t)
    10. {
    11. ++hash2[e-'A'];
    12. kinds++;
    13. }
    14. int left = 0, right = 0, count = 0, n = s.size();//维护窗口用的参数
    15. int minlen = INT_MAX, minleft = -1;//存放最小覆盖子串的参数
    16. while(right < n)
    17. {
    18. hash1[s[right]-'A']++;//进窗口
    19. //判断是否是有效字符
    20. if(hash1[s[right]-'A'] <= hash2[s[right]-'A'])
    21. count++;
    22. //出窗口
    23. while(left
    24. {
    25. //更新结果
    26. minlen = min(minlen, right-left+1);
    27. minleft = (minlen == (right-left+1) ? left : minleft);
    28. //更新有效字符个数
    29. if(hash1[s[left]-'A'] <= hash2[s[left]-'A'])
    30. count--;
    31. hash1[s[left++]-'A']--;
    32. }
    33. right++;
    34. }
    35. if(minleft == -1)
    36. return string();
    37. else
    38. return s.substr(minleft, minlen);
    39. }
    40. };

    也可以去判断有效字符的种类, 只需要修改一下kinds初始化的方式和修改count的条件判断, 判断有效字符个数时用 小于等于 去判断, 只要小于等于就是有效字符;判断有效字符种类的时候只有 等于 的时候才能算一种有效字符统计完全了:

     


  • 相关阅读:
    植物大战僵尸各种僵尸攻略(四)
    【C++初阶】C++入门(一)
    功率放大器在磁性微纳米颗粒微流体操控研究中的应用
    查询不为空的字段
    众和策略:美国芯片出口管制升级,万亿AI巨头回应!一度跌超7%
    【网络安全】黑客自学笔记
    疫情可视化part3
    万界星空科技可视化数字大屏应用场景及作用
    【FPGA教程案例76】通信案例2——基于FPGA的滑动窗口累加器实现
    抽象工厂模式
  • 原文地址:https://blog.csdn.net/ZZY5707/article/details/134459331