刷滑动窗口算法,看这篇文章就够了!

424 阅读5分钟

现在不论是校招还是社招都有机试,一般都是牛客和LeetCode的题型,因此准备找工作前肯定要刷下,在刷查找连续的数据中的子数组或者子串,又或者计算连续的值这种题时,最简单方法就是暴力破解,比方说: 无重复字符的最长子串 这道题要找字符串中无重复的最长字符串,最简单方式就是遍历整个字符串,遍历的内部从当前索引位置的下一位开始再遍历整个字符串,当遇到重复字符时记录并比较是否是最长,也能得出结果,

public int lengthOfLongestSubstring2(String s) {
 
    char[] chars = s.toCharArray();             
    
    StringBuilder temp = new StringBuilder();
    for (int i = 0; i < chars.length; i++) {
            
        for (int j = i+1; j < chars.length; j++) {
             //判断当前截取的字符串中是否包含了该字符,如果包含,比较字符串长度并替换。
        }
    }
    return temp.length();
}

这种方式可以满足要求,但是实现复杂度就高了,有两个循环嵌套,时间复杂度为O(N^2),在运行时很有可能因为超时而通不过测试。

尽管可以增加写边界判断来减少遍历范围,但是还是可能会超时,而且会使代码逻辑变得更复杂,考虑不全很容易出错。

既然这种行不通那就要想办法降低时间复杂度了,最好是能将嵌套for循环改成单循环,这时就需要用到算法的思维方式了,使用滑动窗口算法,就可以做到!

tips:这里所说的算法是计算机的算法,不是想算法工程师的那种算法。前者依然使用的是电脑计算,本质上还是通过电脑暴力破解,只是优化的操作流程,减少了时间复杂度;而后者的算法就是纯纯的数学,电脑只是工具。看经常有人搞混这两个概念,以为学会了算法就可以当算法工程师了,再次特意说下。

滑动窗口算法

作用

  • 主要用于处理连续的数组数据或者字符串数据,常用以解决数组/字符串的子元素问题。
  • 可以将暴力破解的嵌套for循环问题,转换为单循环问题,时间复杂度是 O(N),比暴力算法要高效得多。

可以搞定哪些题型

处理那些从给定的连续数组或者集合中找到特性的连续子串或者统计连续的值时使用。因为滑动窗口本身就是维护连续的数据,因此只能在处理连续的子串时用到。

核心代码/公式

核心的框架内容很少,本质上就是用特定的条件动态的维护窗口,难点是在于要根据不同的题意来书写特定的条件及其他的判断(这里也是最容易出错的地方),再根据需要结合变量存储或哈希存储即可。

//设置滑动窗口左右两侧索引位置
int left = 0;
int right = 0;
// 根据题目需要增加存储类型,可以是变量也可以是hashMap
HashMap<Character, Integer> windows = new HashMap<>();
while (right < s.length()) {
//通过将滑动窗口右侧索引右移来扩大窗口
    right++;
    //todo:进行扩大窗口的一系列处理
    ......
    
    while (判断窗口是否需要收缩) {
        //通过将滑动窗口左侧索引右移来缩小窗口
        left++;
        //todo:缩小窗口的一系列处理
        ......
    }
}

整体思路

  1. 设置滑动窗口的区间是 [left,right) 左闭右开区间。 左开右开会导致缺少元素,例如:(0,1)中没有元素;左闭右闭会导致初始化时就已经存在元素,例如: [0,0] 初始状态时,窗口中已经包含0元素了。
  2. 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。(该步骤的判断要结合题意,容易因为考虑不全出现问题)
  3. 直到符合题目要求。
  4. 再通过将滑动窗口左侧索引右移来缩小窗口,left++。
  5. 满足条件就更新数据,直到不符合就跳出。(该步骤的判断要结合题意,容易因为考虑不全出现问题,处理方式和第2步相对应)
  6. 再次扩大窗口,重复上述操作(2-5步),直至数据末尾,结束跳出,返回相应结果。

实战操作

下面以三道题为例,具体讲解下如果套用思路和代码来搞定这类题型。

  • 3. 无重复字符的最长子串 image.png

这是一道典型的滑动窗口题,要求从连续的字符串中找到无重复的字符,核心是通过判断加入的值是否在窗口中有重复来进行窗口的缩小,保证窗口内部无重复字符串

解题步骤

  1. 设置滑动窗口的区间是 [left,right) 左闭右开区间。 image.png
  2. 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。 image.png
  3. 直到符合题目要求。 image.png
  4. 发现已经出现重复字符,不满足要求时再通过将滑动窗口左侧索引右移来缩小窗口,left++。 image.png
  5. 满足条件就更新数据,直到不符合就跳出。
  6. 再次扩大窗口,重复上述操作(2-5步),直至数据末尾,结束跳出,返回相应结果。

20220826_182751 -big-original.gif

public int lengthOfLongestSubstring(String s) {
// 1 设置滑动窗口的区间是 [left,right)  左闭右开区间。
    int left = 0;
    int right = 0;
    //通过变量记录最大长度
    int len = 0;
    //通过hashMap存储窗口内的字符
    HashMap<Character, Integer> windows = new HashMap<>();
    char[] chars = s.toCharArray();
    while (right < s.length()) {
        //2 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。
        char rightC = chars[right++];
        //3 直到符合题目要求。添加数据到map
        if (windows.containsKey(rightC)) {
            windows.put(rightC, windows.get(rightC) + 1);
        } else {
            windows.put(rightC, 1);
        }
        
        // 4 发现已经出现重复字符,不满足要求时再通过将滑动窗口左侧索引右移来缩小窗口,left++。
        // 当前的值在窗口中不止一个时,缩小窗口,直至当前值在窗口中无重复
        while (windows.get(rightC) > 1) {
            //5 满足条件就更新数据,直到不符合就跳出。
            char leftC = chars[left++];
            windows.put(leftC, windows.get(leftC) - 1);
        }
        
        len = Math.max(len, right - left);


    }
    return len;
}
  • 76.最小覆盖子串

image.png

核心思路就是判断窗口内是完全匹配,因此需要记录匹配的字符和数量以及已经完全匹配的有效值,当有效值和匹配字符的数量一致时,说明全部匹配到,记录最小子串,并缩小窗口至不匹配为止

解题步骤

1.设置滑动窗口的区间是 [left,right) 左闭右开区间。 image.png 2. 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。 image.png 3. 直到符合题目要求。 image.png 4.再通过将滑动窗口左侧索引右移来缩小窗口,left++。

image.png 5.满足条件就更新数据,直到不符合就跳出。

6.再次扩大窗口,重复上述操作(2-5步),直至数据末尾,结束跳出,返回相应结果。

20220826_193253 -big-original.gif

public static String minWindow(String s, String t) {
    //1.设置滑动窗口的区间是 [left,right)  左闭右开区间。
    int left = 0;
    int right = 0;
    String result = "";
    //设置需要匹配内容和有效值
    int valid = 0;
    HashMap<Character, Integer> needs = new HashMap<>();
    HashMap<Character, Integer> windows = new HashMap<>();
    char[] chars = t.toCharArray();
    //填充need
    for (int i = 0; i < chars.length; i++) {
        char c = chars[i];
        if (needs.containsKey(c)) {
            needs.put(c, needs.get(c) + 1);
        } else {
            needs.put(c, 1);
        }
    }
    //扩大窗口  
    char[] sChars = s.toCharArray();
    while (right < s.length()) {
            //2. 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。
        char rChar = sChars[right];
        if (needs.containsKey(rChar)) {
            if (windows.containsKey(rChar)) {
                windows.put(rChar, windows.get(rChar) + 1);
            } else {
                windows.put(rChar, 1);
            }
            // 3. 直到符合题目要求。
            // 如果完全匹配该字符就让有效值+1
            if (windows.get(rChar).equals(needs.get(rChar))) {
                valid++;
            }
        }
        //4.再通过将滑动窗口左侧索引右移来缩小窗口,left++。
        // 判断有效值是否全部匹配need
        while (valid == needs.size()) {
                //5.满足条件就更新数据,直到不符合就跳出
                String temp = s.substring(left, right + 1);
                if(result.equals("")){
                    result = temp;
                }else {
                    result = result.length() < temp.length() ? result : temp;
                }
            //根据左侧是否是匹配字符来修改有效值和窗口内的数据
            char lChar = sChars[left];
            if (needs.containsKey(lChar)) {
                if (windows.get(lChar).equals(needs.get(lChar))) {
                    valid--;
                }
                windows.put(lChar, windows.get(lChar) - 1);
            }
            left++;
        }
        right++;
    }
    return result;
}
  • 乘积小于 K 的子数组
给你一个整数数组 `nums` 和一个整数 `k` ,请你返回子数组内所有元素的乘积严格小于 `k` 的连续子数组的数目。
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10][5][2],、[6][10,5][5,2][2,6][5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

这种题型其实也是变相的求满足条件的所有子串,注意在记录符合的数量时要将子串的所有子串也进行计算,因为当前子串如果满足条件,那其子串也一定满足。

public static int slidingWindow(int[] nums, int k) {
    //1.设置滑动窗口的区间是 \[left,right)  左闭右开区间。
    int left = 0;
    int right = 0;
    //满足条件的数量及窗口内乘积的值。
    int valid = 0;
    int windows = 1;
    while (right < nums.length) {
        //2. 不断将滑动窗口右侧索引向右移动来扩大窗口,right++。
        // 3. 直到符合题目要求。 
        //扩大窗口
        int rightI = nums[right];
        windows *= rightI;
        //4.再通过将滑动窗口左侧索引右移来缩小窗口,left++。
        //当窗口内的乘积不满足条件时,缩小窗口
        while (windows >= k) {
            int leftI = nums[left++];
            windows /= leftI;
        }
        //5.满足条件就更新数据,直到不符合就跳出。
        //统计满足条件的所有子串
        valid += right - left + 1;
        right++;
    }
    return valid;
}

总结

滑动窗口算法 = 滑动窗口固定公式+(变量/HashMap)+ 根据题目进行的窗口扩大/缩小的操作

通过滑动窗口算法可以处理连续的数组数据或者字符串数据,难点在于如何判断和计算,调节窗口,上面三道题虽然是使用的是同一种算法,但是细节方面差异很大,都要去领会题意,只能是多接触不同的题型才能熟练。希望能给大家一些帮助,祝大家刷题无压力!!!