• 算法——滑动窗口


    1. 什么是滑动窗口

    滑动窗口算法是一种解决数组或列表中子数组或子序列问题的有效方法。该算法通过定义一个窗口,然后在数据结构上滑动该窗口,逐步处理数据,以解决特定类型的问题。其基本思想是维护一个窗口,初始时窗口覆盖数组中的一部分元素,然后通过滑动窗口来依次处理每个子数组。在每次窗口滑动时,可以通过添加新元素和删除旧元素来更新窗口的内容,以在O(1)时间内完成操作。

    2. 滑动窗口的大致解题流程

    滑动窗口实际上是双指针算法的一个延伸,它特指双指针算法中的同向双指针,在这里我们以一道例题来讲解滑动窗口类型的大致解题流程,例题如下:209. 长度最小的子数组 - 力扣(LeetCode)

    由于在实际的做题中,我们并不能总是直接判断这道题能用什么算法,因此我们可以先用暴力解法解决这道题,即固定一个左指针,从左指针的位置向右遍历数组,遍历到和为target为止(第一个即可),随后将左指针右移,再次执行上述操作,随时统计最小的区间,稍微分析这个算法我们可以发现它的时间复杂度是O(N^2)的,在这个过程中我们可以看到,整个数组是严格单调的且指针移动方向是相同的,此时我们就可以采取滑动窗口的思想来解决,滑动窗口一般流程如下

    1. 进窗口

    2. 判断

            出窗口

    3. 在合适的位置更新结果

    在这道题中我们可以发现,当右指针处于一个新位置时,将其所在元素入窗口,直到sum(窗口内元素和)>= target时,将左指针位置出窗口,并记录当前区间 ,稍加分析可以发现更新结果的位置应该在出窗口前,这样的话我们可以得到如下代码

    1. class Solution
    2. {
    3. public:
    4. int minSubArrayLen(int target, vector<int>& nums)
    5. {
    6. int len = INT_MAX;
    7. int n = nums.size(), sum = 0;
    8. for (int left = 0, right = 0; right < n; right++)
    9. {
    10. sum += nums[right];
    11. while (sum >= target)
    12. {
    13. len = min(len, right - left + 1);
    14. sum -= nums[left++];
    15. }
    16. }
    17. return len == INT_MAX ? 0 : len;
    18. }
    19. };

    3. 应用实例

    1. 无重复字符的最长子串

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

    解析:分析题目我们可以发现,这道题是要寻找最长的不含重复字符的子串,我们可以采取哈希表统计字符个数,滑动窗口控制子串长度的操作,分析一下可以知道,当right处于一个新位置就将其入窗口,若当前字符的出现次数 > 1则表明当前字符串出现重复字符,此时让left所在位置的元素出窗口,直到当前字符出现次数 == 1,显然,更新结果应在出窗口后,代码如下

    1. class Solution
    2. {
    3. public:
    4. int lengthOfLongestSubstring(string s)
    5. {
    6. int len = 0, n = s.size();
    7. unordered_map<char, int> hash;
    8. for (int left = 0, right = 0; right < n; right++)
    9. {
    10. char in = s[right];
    11. hash[in]++;
    12. while (hash[in] > 1)
    13. {
    14. char out = s[left++];
    15. hash[out]--;
    16. }
    17. len = max(len, right - left + 1);
    18. }
    19. return len;
    20. }
    21. };

    2. 最大连续1的个数

    题目链接:485. 最大连续 1 的个数 - 力扣(LeetCode)

    解析:分析题目我们可以发现,这道题是要寻找最长的只含有1的数组,我们可以直接滑动窗口统计长度的操作,分析一下可以知道,当right处于1的位置就将其入窗口,若right处于0的位置,则表明当前数组不是仅含有0的数组,此时让left=right+1出窗口,显然,更新结果应在出窗口前,代码如下

    1. class Solution
    2. {
    3. public:
    4. int findMaxConsecutiveOnes(vector<int>& nums)
    5. {
    6. int len = 0, n = nums.size();
    7. for (int left = 0, right = 0; right < n; right++)
    8. {
    9. if (nums[right] == 1) len = max(len, right - left + 1);
    10. else left = right + 1;
    11. }
    12. return len;
    13. }
    14. };

    3. 将x减到0的最小操作数

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

    解析:对于这道题分析后我们可以发现,正面解决可能比较困难,此时我们就可以采取正难则反的思想,这道题是要寻找将x减到0的最小操作数,换而言之,就是寻找值为sum-x的最长连续子区间,我们可以使用滑动窗口来寻找指定区间,分析一下可以知道,当right处于一个新位置就将其入窗口,若当前区间的总和 >= sum-x,则表明当前数组可能是一个合理区间,此时让left所在位置的元素出窗口,直到当前区间总和 < sum-x,显然,更新结果应在出窗口前,代码如下

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

    4. 水果成篮

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

    解析:简单翻译一下这道题的题意就是仅能有2种树被采摘,求最多能采摘多少树,显然我们可以使用滑动窗口 + 哈希表的解法,分析一下可以知道,当right处于一个新位置就将其入窗口,若当前哈希表中的种类数量 > 2,则表明当前这些树超过了2个种类,此时让left所在位置的元素出窗口,直到哈希表中的种类数量 == 2,显然,更新结果应在出窗口后,代码如下

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

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

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

    解析:对于这道题我们是要在s中找到所有p的异位词,即需要遍历一遍s,我们可以使用两个哈希表,来分别统计s与p中对应字符的个数,当right处于新位置时入窗口,当当前窗口大小size > p.size时,让left所处位置出窗口,更新结果当然是在出窗口后,代码如下

    1. class Solution
    2. {
    3. public:
    4. vector<int> findAnagrams(string s, string p)
    5. {
    6. vector<int> ret;
    7. int hash1[26] = {0};
    8. for (auto ch : p) hash1[ch-'a']++;
    9. int hash2[26] = {0};
    10. for (int left = 0, right = 0; right < s.size(); right++)
    11. {
    12. char in = s[right];
    13. hash2[in - 'a']++;
    14. if (right - left + 1 > p.size()) hash2[s[left++]-'a']--;
    15. if (check(hash1, hash2)) ret.push_back(left);
    16. }
    17. return ret;
    18. }
    19. bool check(int* hash1, int* hash2)
    20. {
    21. for (int i = 0; i < 26; i++)
    22. if (hash1[i] != hash2[i]) return false;
    23. return true;
    24. }
    25. };

    此外,我们可以对这个算法进行一个优化——即使用变量count来统计有效字符数,即当一个right即将入窗口时,若hash2中它所对应的字符 < hash1中它所对应的字符时,count++,当一个left即将出窗口时,若hash2中它对应的字符 <= hash1中它所对应的字符时,count--,代码如下

    1. class Solution
    2. {
    3. public:
    4. vector<int> findAnagrams(string s, string p)
    5. {
    6. int count = 0;
    7. vector<int> ret;
    8. int hash1[26] = {0};
    9. for (auto ch : p) hash1[ch-'a']++;
    10. int hash2[26] = {0};
    11. for (int left = 0, right = 0; right < s.size(); right++)
    12. {
    13. char in = s[right];
    14. if (hash2[in-'a'] < hash1[in-'a']) count++;
    15. hash2[in-'a']++;
    16. if (right - left + 1 > p.size())
    17. {
    18. char out = s[left++];
    19. if (hash2[out - 'a'] <= hash1[out-'a']) count--;
    20. hash2[out-'a']--;
    21. }
    22. if (count == p.size()) ret.push_back(left);
    23. }
    24. return ret;
    25. }
    26. };

    6. 串联所有单词的子串

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

    解析:由于word中所有字符串长度相等,我们可以计算出word.lenth * word[i].lenth来计算出排列总数,然后再使用count计数的原理(详见上题结尾),在一个right(这里的right代指一串子串)入窗口时,若hash2[right] < hash1[right]则count++,当窗口长度 > word.lenth * word[i].lenth时,让left出窗口,若hash2[left] <= hash1[left]则count--,结果在出窗口后更新,代码如下

    1. class Solution
    2. {
    3. public:
    4. vector<int> findSubstring(string s, vector& words)
    5. {
    6. int len = words.size() * words[0].size();
    7. unordered_mapint> hash1;
    8. for (auto& s : words) hash1[s]++;
    9. int n = s.size();
    10. vector<int> ret;
    11. for (int i = 0; i < words[0].size(); i++)
    12. {
    13. unordered_mapint> hash2;
    14. int count = 0;
    15. for (int left = i, right = i; right < n; right += words[0].size())
    16. {
    17. string in = s.substr(right, words[0].size());
    18. // 使用hash1.count(in)可以避免创建新的映射关系,从而提高效率
    19. if (hash1.count(in) && hash2[in] < hash1[in]) count++;
    20. hash2[in]++;
    21. if (right - left + 1 > len)
    22. {
    23. string out = s.substr(left, words[0].size());
    24. if (hash2[out] <= hash1[out]) count--;
    25. hash2[out]--;
    26. left += words[0].size();
    27. }
    28. if (count == words.size()) ret.push_back(left);
    29. }
    30. }
    31. return ret;
    32. }
    33. };

    7. 最小覆盖子串

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

    解析:分析这道题我们可以发现,这道题是要在s中寻找最小的子串使他能覆盖t,我们可以使用滑动窗口来寻找指定区间,哈希表来统计对应字符出现个数,分析一下可以知道,当right处于一个新位置就将其入窗口,若当区间有效字符种类数count==kind(t中字符种类个数),则表明当前子串属于合理区间,此时让left所在位置的元素出窗口,直到当前区间有效字符count < kind,显然,更新结果应在出窗口前,代码如下

    1. class Solution
    2. {
    3. public:
    4. string minWindow(string s, string t)
    5. {
    6. if (s.size() < t.size()) return "";
    7. int kind = 0;
    8. int hash1[126] = {0};
    9. for (auto ch : t) if (hash1[ch]++ == 0) kind++;
    10. int hash2[126] = {0};
    11. int len = INT_MAX, begin = -1;
    12. for (int left = 0, right = 0, count = 0; right < s.size(); right++)
    13. {
    14. char in = s[right];
    15. hash2[in]++;
    16. if (hash2[in] == hash1[in]) count++;
    17. while (count == kind)
    18. {
    19. if (right - left + 1 < len)
    20. {
    21. len = right - left + 1;
    22. begin = left;
    23. }
    24. char out = s[left++];
    25. if (hash2[out] == hash1[out]) count--;
    26. hash2[out]--;
    27. }
    28. }
    29. if (begin == -1) return "";
    30. else return s.substr(begin, len);
    31. }
    32. };
  • 相关阅读:
    Linux配置telnet服务端
    网络安全法学习
    Redis 主从复制 + 哨兵模式 + Cluster 集群
    Adobe XD最新版号查询,如何使用?
    优选丨 5 大用例设计笔试大题,附超详细解析
    【kafka】kafka常见的面试题总结及对应答案
    数字化转型这么火热,企业选择数字化转型都有哪些目的
    使用 Git 工具进行项目管理
    Spring 类路径扫描和组件托管
    【Python零基础入门篇 · 16】:拆包、异常种类、异常处理、抛出异常
  • 原文地址:https://blog.csdn.net/qq_73201597/article/details/136290459