滑动窗口详解

简介: 本文介绍了滑动窗口算法及其应用。具体题目有: 1. 长度最小的子数组(LeetCode 209)。 2. 无重复字符的最长子串(LCR 016)。 3. 最大连续 1 的个数 III(LeetCode 1004)。 4. 将 x 减到 0 的最小操作数(LeetCode 1658)。 5. 水果成篮(LeetCode 904)。 6. 找到字符串中所有字母异位词(LeetCode 438)。 7. 串联所有单词的子串(LeetCode 30)。 8. 最小覆盖子串(LeetCode 76)。 每题详细分析了暴力解法及滑动窗口优化方案,附带代码实现。

滑动窗口其实也就是之前介绍的同向双指针

步骤:

  1. 定义 left = 0, right = 0
  2. 进窗口
  3. 判断   出窗口   更新结果(更新结果的步骤根据具体题目中的要求进行)

1. 长度最小的子数组

209. 长度最小的子数组

暴力解法:枚举出所有的情况,然后判断长度和区间和,这种方法的时间复杂度最优为O(n^2)

优化:利用单调性,使用同向指针优化

思路:定义left = 0,right = 0,right先往右移动,直到区间内的和符合条件,然后left往右移动并判断是否符合条件

当right移动到刚好符合条件的位置时,后面的就没必要进行枚举了,并且right也没有必要再从头再来枚举所有的情况了

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0, right = 0, sum = 0;
        int min = 100001;
        while (right < nums.length) {
            sum += nums[right];
            while (target <= sum) {
                min = Math.min(min, right - left + 1);
                sum -= nums[left];
                left++;
            }
            right++;
        }
        return (min == 100001) ? 0 : min;
    }
}

2. LCR 016. 无重复字符的最长子串

LCR 016. 无重复字符的最长子串

暴力解法:从第一个字符开始,固定每一个起始位置,一直往后枚举到出现重复字符,计算子串长度
                再从下一个位置继续往后枚举,最终从枚举到的所有子串中找到最长的
                判断重复元素时可以利用哈希表来判断(后面的题经常要用到这个技巧)

思路:在暴力解法中可以发现,当遇到第一个重复字符时,只需要把left往后移动,跳过这个字符就可以继续往下枚举了,并且期间跳过的子串肯定是没有第一次枚举的子串长的,

还发现,当left跳过重复字符之后,right可以在原来的位置继续往后移动,不用再回过头来重新枚举字符串,这些子串的长度肯定也还是没有原来的长的,这样就又可以使用同向指针构成滑动窗口来解决这道题了

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int[] hash = new int[128];
        char[] chars = s.toCharArray();
        int max = 0;
        int left = 0, right = 0;
        while (right < s.length()) {
            //进窗口
            hash[chars[right]]++;
            //判断出窗口
            while (hash[chars[right]] > 1) {
                hash[chars[left]]--;
                left++;
            }
            //更新结果
            max = Math.max(max, right - left + 1);
            right++;
        }
        return max;
    }
}

3. 最大连续1的个数 III

1004. 最大连续1的个数 III

翻转0的操作可以转化为查找一个区间最多有k个0,如果这个区间的0的个数在k个以内,那肯定就都可以翻转

暴力解法:还是通过两层for循环,依次枚举所有的可能,比较每种可能的长度

优化:通过双指针left,right,依次先确定left的位置,right向右移动,区间内有k个0之后,再把left右移,确定下一个区间,这时可以发现,如果left的位置不是0的话,再枚举k个0的区间肯定没有第一次枚举的区间长

第一个优化:left右移确定下一个起始位置时,right不用再回来

第二个优化:可以直接把left移动到下一个0的位置,此时right就可以往后移动

class Solution {
    public int longestOnes(int[] nums, int k) {
        int flag = 0, ans = 0;//flag为0的个数
        for (int left = 0, right = 0; right < nums.length; right++) {
            //进窗口
            if (nums[right] == 0) {
                flag++;
            }
            //判断出窗口
            while (flag > k) {
                if (nums[left++] == 0) {
                    flag--;
                }
            }
            ans = Math.max(right - left + 1, ans);
        }
        return ans;
    }
}

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

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

如果按照题目中的思路直接去考虑两端的数据就不太好想,所以换一种方式,考虑剩余的数据,左右两端的数据加起来等于x,那么中间的数据就等于nums数组和 - x ,也就是找出一段子串和等于 sum - x,再去比较出长度最长的子串,所以就转化为了使用滑动窗口求满足一定条件长度最长的子串的问题了

class Solution {
    public int minOperations(int[] nums, int x) {
        int sum = 0;
        int ans = -1;
        for (int a : nums)
            sum += a;
        if (sum < x)
            return -1;
        sum -= x;
        for (int left = 0, right = 0, tmp = 0; right < nums.length; right++) {
            tmp += nums[right];
            while (tmp > sum) {
                tmp -= nums[left++];
            }
            if (tmp == sum)
                ans = Math.max(right - left + 1, ans);
        }
        return (ans == -1) ? -1 : nums.length - ans;
    }
}

5. 水果成篮

904. 水果成篮

这道题就是求满足一定条件的最长的子数组的问题

思路:使用哈希表来存储元素出现的次数,以此判断存储元素的种类,然后利用滑动窗口更新答案值

优化:如果使用HashMap的话,虽然说结果好存储,但是运行效率不高,所以采用数组模拟哈希表的方法,定义一个kind变量,来统计当前篮子中的水果数量

class Solution {
    public int totalFruit(int[] fruits) {
        int ans = 0;
        int[] hash = new int[fruits.length + 5];
        for (int left = 0, right = 0, kind = 0; right < fruits.length; right++) {
            if (hash[fruits[right]] == 0) {
                kind++;
            }
            hash[fruits[right]]++;
            while (kind > 2) {
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0) {
                    kind--;
                }
                left++;
            }
            ans = Math.max(ans, right - left + 1);
        }
        return ans;
    }
}

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

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

暴力解法:创建两个哈希表,然后把字符串p里面的每个字符存里面,接着遍历枚举所有s里面的和字符串p长度相等的子串,再把子串也存到哈希表中,对比两个哈希表中的值是否相等

滑动窗口优化:在枚举所有组合的过程中发现,因为需要维护子串的长度,所以如果right向右移动,left也要向右

移动,并且,存放到哈希表中的数据只是受到了left和right两个位置的影响,所以就可以使用滑动窗口进行枚举

判断条件优化:由于这道题只有26个字符,所以每次都遍历哈希表进行判断也可以,但是还可以进行优化

可以定义一个计数器cnt来统计枚举出来的组合的有效元素,在遍历的过程中,如果hashs中的数据大于hashp里的就代表出现了重复的元素,此时cnt就不用增加,在出窗口的过程中,如果hashs中的数据小于hashp里的,就表示此时滑出窗口的元素是一个有效值,cnt就需要减一,最后判断有效数据的个数是否等于字符串p的长度来更新结果

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        //存储结果
        List<Integer> ans = new ArrayList<>();
        int[] hashP = new int[26];
        char[] pp = p.toCharArray();
        //把字符串p存放到哈希表中
        for (char c : pp) {
            hashP[c - 'a']++;
        }
        int[] hashS = new int[26];
        char[] ss = s.toCharArray();
        for (int left = 0, right = 0, cnt = 0; right < ss.length; right++) {
            //进窗口 + 判断有效字符个数cnt
            if (++hashS[ss[right] - 'a'] <= hashP[ss[right] - 'a']) {
                cnt++;
            }
            //判断+出窗口
            if (right - left + 1 > pp.length) {
                if (hashS[ss[left] - 'a']-- <= hashP[ss[left] - 'a']) {
                    cnt--;
                }
                left++;
            }
            //有效字符相等,更新结果
            if (cnt == p.length()) {
                ans.add(left);
            }
        }
        return ans;
    }
}

7. 串联所有单词的子串

30. 串联所有单词的子串

这道题其实和找出所有字母异位词特别像,只不过这道题把字母换成了字符串而已,那么就不能再使用普通的数组模拟哈希表来存储了,需要使用到容器来存储每一个字符串和出现的次数,然后就是一些细节问题的处理

  1. 由于是字符串,所以需要进行多次的滑动窗口,具体的次数根据给出的字符串数组中的字符串长度来看
  2. 注意,每一次使用滑动窗口都要重新创建一个哈希表
  3. 在更新cnt的时候,要注意此时最开始的字符串可能不在hash1中,直接调get方法会空指针异常
class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ans = new ArrayList<>();
        //把words放进哈希表中
        Map<String, Integer> hash1 = new HashMap<>();
        for (String word : words) {
            hash1.put(word, hash1.getOrDefault(word, 0) + 1);
        }
        int len = words[0].length();
        //外层表示执行滑动窗口的次数
        for (int i = 0; i < len; i++) {
            //每次都要创建新的哈希表
            Map<String, Integer> hash2 = new HashMap<>();
            for (int left = i, right = i, cnt = 0; right + len <= s.length(); right += len) {
                //截取字符串,添加到哈希表中
                String str = s.substring(right, right + len);
                hash2.put(str, hash2.getOrDefault(str, 0) + 1);
                //hash1中也使用getOrDefault判断,可以避免空指针
                if (hash2.get(str) <= hash1.getOrDefault(str, 0)) {
                    cnt++;
                }
                if (right - left + 1 > words.length * len) {
                    String strl = s.substring(left, left + len);
                    if (hash2.get(strl) <= hash1.getOrDefault(strl, 0)) {
                        cnt--;
                    }
                    hash2.put(strl, hash2.get(strl) - 1);
                    left += len;
                }
                if (cnt == words.length) {
                    ans.add(left);
                }
            }
        }
        return ans;
    }
}

8. 最小覆盖子串

76. 最小覆盖子串

暴力解法和上面的几题都很相似,直接来看使用滑动窗口优化之后的版本

思路:使用同向双指针维护一个窗口,使窗口内的子串涵盖字符串 t 所有字符,然后让left指针右移,此时会出现两种情况,一种是第一个和字符串t中字符相等的有很多,就算一个字符出窗口之后剩余的字符还会符合条件,另一种就是不符合条件的情况,需要right指针右移

判断出窗口的情况是窗口内的字符刚好符合题目要求,才开始出窗口,因为需要找到最小子串,那么更新结果也是在判断刚好符合条件之后就要更新结果了

优化:使用变量cnt来标记有效字符的种类,无论是进窗口还是出窗口,更新cnt时都需要保证两个哈希表中该该字符出现的种类数是相等的,否则cnt就会重复计数,最终判断有效字符个数cnt是否和hash1的元素个数相等就可以更新结果了

class Solution {
    public String minWindow(String s, String t) {
        int min = 100000, begin = -1;
        char[] chars = s.toCharArray();
        char[] chart = t.toCharArray();
        int[] hash1 = new int[128];
        int count = 0;
        for (char value : chart) {
            if (hash1[value] == 0) {
                count++;
            }
            hash1[value]++;
        }
        int[] hash2 = new int[128];
        //cnt : 有效字符的种类
        for (int left = 0, right = 0, cnt = 0; right < chars.length; right++) {
            //进窗口
            hash2[chars[right]]++;
            if (hash1[chars[right]] == hash2[chars[right]]) {
                cnt++;
            }
            //判断
            while (cnt == count) {
                //更新结果
                if (right - left + 1 < min) {
                    begin = left;
                    min = right - left + 1;
                }
                //出窗口
                if (hash2[chars[left]] == hash1[chars[left]]) {
                    cnt--;
                }
                hash2[chars[left]]--;
                left++;
            }
        }
        if (begin == -1)
            return new String();
        else
            return s.substring(begin, begin + min);
    }
}
相关文章
|
7月前
|
存储 算法 Java
【算法系列篇】滑动窗口-1
【算法系列篇】滑动窗口-1
|
7月前
|
算法 测试技术 C#
【滑动窗口】C++算法:可见点的最大数目
【滑动窗口】C++算法:可见点的最大数目
|
算法 索引
算法—滑动窗口
滑动窗口算法简介滑动窗口,顾名思义,就是有一个大小可变的窗口,左右两端方向一致的向前滑动(右端固定,左端滑动;左端固定,右端滑动)。可以想象成队列,一端在push元素,另一端在pop元素,如下所示:假设有数组[a b c d e f g h]一个大小为3的滑动窗口在其上滑动,则有:[a b c][b c d][c d e][d e f][e f g][f g h]适用范围1、一般是字符串或...
62 0
|
2月前
【LeetCode 04】滑动窗口法总结
【LeetCode 04】滑动窗口法总结
24 0
|
5月前
|
算法
算法 —— 滑动窗口
算法 —— 滑动窗口
41 2
|
存储 算法 Java
趣味算法:滑动窗口算法的理解与应用
前言 在编程和数据结构中,滑动窗口算法是一种常见的解决问题的方法。它主要用于处理涉及连续或固定长度子数组、子序列或子字符串的问题。本文将深入探讨滑动窗口算法,包括其基本概念、应用场景、基本步骤以及具体的Java代码实践。
607 0
|
7月前
|
算法
滑动窗口(一)
滑动窗口(一)
|
7月前
|
算法 索引 容器
滑动窗口(二)
滑动窗口(二)
|
7月前
|
算法 Java 索引
【算法系列篇】滑动窗口-2
【算法系列篇】滑动窗口-2
|
7月前
|
算法 搜索推荐 测试技术
C++算法:滑动窗口总结
C++算法:滑动窗口总结