1.概述

(1)滑动窗口可以用以解决数组/字符串的子元素相关问题,并且可以将嵌套的循环问题,转换为单循环问题,从而降低时间复杂度。故滑动窗口算法的复杂度一般为 O(n)。

(2)滑动窗口的基本思想如下:

  • 首先使用双指针维护一个子数组,别称为 left 和 right。left 指向窗口的左端点,right 指向窗口的右端点。如下图所示:
    在这里插入图片描述
  • 窗口随着 right 指针向右滑动开始遍历整个数组区间(即增大窗口);

在这里插入图片描述

  • 而在每次迭代内部(即针对每一次 right)要对子数组区间是否满足要求进行判断。如果子数组区间不能够满足条件则将 left 指针向右移动(即缩小窗口),这样窗口就实现了向右滑动。
    在这里插入图片描述

2.算法框架

滑动窗口的算法框架如下:

class Solution {
    /**
     * @param1: 待处理的序列,一般为数组或字符串等线性表
     * @description: 滑动窗口算法框架
     */
    public void slidingWindow(int[] nums) {
        //此处使用 HashMap 来表示滑动窗口,也可根据具体情况使用其它存储结构
        Map<Integer, Integer> window = new HashMap<>();
        int length = nums.length;
        //定义滑动窗口的左右端点,初始值均为 0
        int left = 0;
        int right = 0;
        while (right < length) {
            //即将移入窗口的元素
            int inEle = nums[right];
            //增大窗口
            right++;
            //更新窗口内的数据
            //...
            
            //判断窗口是否要缩小
            while (窗口需要缩小的条件) {
                //即将移出窗口的元素
                int outEle = nums[left];
                //缩小窗口
                left++;
                //更新窗口内的数据
                //...
            }
        }
    }
}

3.应用

(1)下面以 LeetCode 中的76.最小覆盖子串这题为例,来使用上述的滑动窗口框架:

在这里插入图片描述

  • 如果直接使用暴力穷举法,其思路为使用两层 for 循环来枚举 s 的所有子串,并且逐一判断每个子串是否包含字符串 t 中的所有字母,如果包含则更新答案。显然该方法的时间复杂度肯定超过 O(n2),其代码结构大致如下所示:
for (int i = 0; i < s.length(); i++) {
    for (int j = i + 1; j < s.length(); j++) {
        // isIncluded(s, i, j) 用于判断子串 s[i...j] 是否包含字符串 t 中的所有字母
        if (isIncluded(s, i, j, t)) {
            //更新答案
        }
    }
}
  • 使用滑动窗口算法的具体代码实现如下:
class Solution {
    public String minWindow(String s, String t) {
        int sLen = s.length();
        // window 中记录滑动窗口内的字符以及对应出现的次数
        HashMap<Character, Integer> window = new HashMap<Character, Integer>();
        // need 记录字符串 t 中的字符以及对应出现的次数
        HashMap<Character, Integer> need = new HashMap<Character, Integer>();
        //将字符串 t 中的字符存入哈希表 need 中,key/value 为字符/对应出现的次数
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            need.put(c, need.getOrDefault(c, 0) + 1);
        }
        //定义滑动窗口的左右端点,初始值均为 0
        int left = 0;
        int right = 0;
        //valid 记录窗口中 t 中不重复字符的个数
        int valid = 0;
        //记录最小覆盖子串的起始索引以及长度
        int start = 0, minLen = Integer.MAX_VALUE;
        while (right < sLen) {
            char c = s.charAt(right);
            right++;
            //更新窗口内的数据,如果字符 c 存在于 t 中,则将其加入到 window 中
            if (need.containsKey(c)) {
                window.put(c, window.getOrDefault(c, 0) + 1);
                /*
                    如果t中所有的字符c都已经加入到窗口中
                    注意:使用"=="比较 Integer 类型的值时,值的范围只能在 -128 ~ 127 之间,否则会出错
                    详情请见https://www.cnblogs.com/mrhgw/p/10449391.html
                */
                if (window.get(c).equals(need.get(c))) {
                    valid++;
                }
            }
            //判断左侧窗口是否要收缩,t 中的所有字符都已经加入到窗口中
            while (valid == need.size()) {
                //更新最小覆盖子串
                if (right - left < minLen) {
                    start = left;
                    minLen = right - left;
                }
                //d 是即将移出窗口的字符
                char d = s.charAt(left);
                left++;
                //更新窗口内的数据
                if (need.containsKey(d)) {
                    if (window.get(d).equals(need.get(d))) {
                        valid--;
                    }
                    window.put(d, window.get(d) - 1);
                }
            }
        }
        return (minLen == Integer.MAX_VALUE) ? "" : s.substring(start, start + minLen);
    }
}

(2)大家可以去 LeetCode 上找相关的滑动窗口的题目来练习,或者也可以直接查看LeetCode算法刷题目录 (Java)这篇文章中的滑动窗口章节。如果大家发现文章中的错误之处,可在评论区中指出。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐