滑动窗口算法系列|基础概念|例题讲解

大家好,我是LvZi,今天带来滑动窗口算法系列|基础概念|例题讲解
在这里插入图片描述

一.滑动窗口问题基础概念

滑动窗口本质上是同向双指针问题,脱胎于双指针.使用两个指针l, r维护一定长度的数组区间,在r 指针遍历的过程中,执行进窗口,判断,更新结果,出窗口 等操作,当r指针遍历完毕,就能得到最后的结果

滑动窗口算法的代码比较固定,大致是以下步骤:

  1. 进窗口 将元素添加到区间内部 可以使用变量,数组,哈希表维护
  2. 判断 判断添加元素之后,当前区间是否满足要求;如果满足执行出窗口操作
  3. 更新结果 虽然放到第三步,但是更新结果的时机要因题而异

滑动窗口之所以快,是因为实现了一次遍历得到结果,减少了暴力循环带来的冗余操作

1.基本概念

滑动窗口算法是一种高效的算法,用于解决涉及连续子数组或子字符串的问题。它通过维护一个动态窗口来扫描数组或字符串,从而减少重复计算,提高算法效率。这个动态窗口根据性质可以分为两类:

  1. 固定大小的滑动窗口
  2. 可变大小的滑动窗口

2.固定大小的滑动窗口

在固定大小的滑动窗口中,窗口的大小是预先确定的,窗口从左到右逐个滑动。常见问题包括:

  • 最大子数组和(大小固定):找到一个大小为k的子数组,使其和最大。
  • 平均子数组和(大小固定):找到一个大小为k的子数组,使其和的平均值最大。

3.可变大小的滑动窗口

在可变大小的滑动窗口中,窗口的大小是动态变化的,取决于具体问题。常见问题包括:

  • 最小覆盖子串:找到包含所有给定字符的最小子串。
  • 最长无重复字符子串:找到没有重复字符的最长子串。

4.技巧和策略

  • 双指针技术:使用两个指针(l,r),一个指向窗口的起始位置,一个指向窗口的结束位置,以便动态调整窗口大小
  • 哈希表/字典:常用于记录窗口内元素的频率,帮助快速检查条件(例如字符是否满足要求)。也经常会使用数组模拟哈希表
  • 条件判断和滑动窗口的调整:根据问题的要求,动态调整窗口的大小和位置。
  • 其实也不用纠结使用定长还是不定长的,只要分析出题目是使用滑动窗口解决就行;窗口的定长还是不定长影响的是更新结果的时机,而这个时机根据具体题目具体判断即可
  • 还有最重要的一点是:判断是否能使用滑动窗口(同向双指针)的关键点在于数组是否具有单调性,注意不是数组元素严格的单增单减,要结合题目所求

二.例题讲解

1.⻓度最⼩的⼦数组

⻓度最⼩的⼦数组
在这里插入图片描述

分析

  • 最简单的方法就是暴力查找,但是会超时

滑动窗口解法

  1. 进窗口 使用sum来维护区间和 遍历到一个数字就加
  2. 判断 判断sum是否大于等于target 如果成立 更新结果 +出窗口
  • 本题的单调性在于:数组中的元素都是正数,随着指针的移动,和一定是越来越大的

在这里插入图片描述

代码

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length, sum = 0, len = 0x3f3f3f3f;
        for(int i = 0, j = 0; j < n; j++) {
            sum += nums[j];// 进窗口
            while(sum >= target) {// 判断
            	// 判断成立  更新结果 + 出窗口
                len = Math.min(len, j - i + 1);
                sum -= nums[i++];
            }
        }

        return len == 0x3f3f3f3f ? 0 : len;
    }
}

2.⽆重复字符的最⻓⼦串

⽆重复字符的最⻓⼦串
在这里插入图片描述
分析

● 经典的滑动窗口问题,本题的一个技巧在于使用数组模拟哈希表

  1. 进窗口 添加字符 字符数组记录字符出现的次数
  2. 判断 判断添加的字符的出现次数是否>2,如果大于则是重复字符,出窗口
  3. 更新结果 每次添加进字符就更新一次结果

s的ASCII码范围是0-128,所以可以使用大小为128的数组模拟哈希表
代码

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

        return len;
    }
}

3.最⼤连续 1 的个数 III

最⼤连续 1 的个数 III
在这里插入图片描述
分析

  • 采用转化思想,如果考虑翻转/改变数组,比较麻烦,可以转化为统计区间内部0的个数,只要保证区间内部0的个数不超过k,就一定能翻转成功

滑动窗口思路

  1. 进窗口 使用二进制数字统计当前数字出现的次数
  2. 判断 判断0出现的次数是否超过k,如果超过,出窗口
  3. 更新结果 每遍历到一个数字就更新一次结果

代码

  • 方法一:使用计数器统计0的数量
class Solution {
    public int longestOnes(int[] nums, int kk) {
        // 使用计数器统计数量  有点抽象
        int slow = 0, fast = 0, ret = -1, n = nums.length, k = kk;
        while(fast < n) {
            if(nums[fast] == 0) k--;
            while(k < 0) {
                if(nums[slow++] == 0) k++;
            }

            ret = Math.max(ret, fast - slow + 1);
            fast++;
        }
        return ret;
    }
}
  • 方法二:使用二进制数组
class Solution {
    public int longestOnes(int[] nums, int k) {
        // 二进制数组  0下标存储0出现的次数  1下标存储1出现的次数
        int[] arr = new int[2];
        int slow = 0, fast = 0, ret = -1, n = nums.length;
        while(fast < n) {
            arr[nums[fast]]++;
            while(arr[0] > k)
                if(nums[slow++] == 0)
                    arr[0]--;
            ret = Math.max(ret, fast - slow + 1);
            ++fast;
        }
        return ret;
    }
}

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

将 x 减到 0 的最⼩操作数

在这里插入图片描述

分析

  • 从左右两端选择最少个数的数字,使得和恰好等于x
  • 转化为:从数组中挑选连续区间的数字,使得和恰好等于target的最长的区间.和第一题类似,这里求的是满足条件下的最长的子数组

代码

class Solution {
    public int minOperations(int[] nums, int x) {
        int sum = 0;
        for(int n : nums) sum += n;
        int target = sum - x;
        if(target < 0) return -1;// nums全是正数

        int l = 0, r = 0, tmp = 0, n = nums.length, ret = -1;
        while(r < n) {
            tmp += nums[r];// 进窗口

            while(tmp > target) {// 判断
                tmp -= nums[l++];// 判断成立 出窗口
            }

            if(tmp == target)// 更新结果
                ret = Math.max(ret, r - l + 1);
            ++r;
        }

        if(ret == -1) return -1;
        return n - ret;
    }
}

5.水果成篮

水果成篮
在这里插入图片描述
分析
使用kinds记录区间内部苹果种类的个数

  1. 进窗口 增加对应种类苹果的数量 如果是新种类,kinds++;
  2. 判断 判断kinds > 2,如果大于,出窗口
  3. 更新结果

代码

class Solution {
    public int totalFruit(int[] fruits) {
        // 从左往右找  满足只有两个种类苹果的最大数目  滑动窗口
        int l = 0, r = 0, n = fruits.length, ret = -1, kinds = 0;
        int[] hash = new int[n + 1];// 统计种类的数目

        while(r < n) {
            if(hash[fruits[r]] == 0) ++kinds;// 判断是否是新种类
            hash[fruits[r]]++;// 进窗口

            // 判断
            while(kinds > 2) {
                hash[fruits[l]]--;
                if(hash[fruits[l]] == 0) kinds--;// 数量为0  种类减1
                ++l;
            }

            // 更新结果
            ret = Math.max(ret, r - l + 1);
            ++r;
        }

        return ret;
    }
}

6.找到字符串中所有字⺟异位词(固定窗口大小)

找到字符串中所有字⺟异位词

在这里插入图片描述
分析

  • 难点在于如何判断两个指针区间的字符串和p字符串是否满足异位词
  • 如果是异位词,则字母类型及个数完全相等,可以考虑使用两个哈希表记录字母及出现的频数,但是要求窗口内部的字符必须都在p中 ,所以使用cnt来记录有效字符的个数
  • 由于都是小写字母,考虑使用数组模拟哈希表,通过数组记录字母出现的频数
  • 本题是一个固定大小的滑动窗口问题,大小等于字符串p的长度,应该保证窗口的大小始终不标
  1. 进窗口 将对应字符的频数加1,并判断是否是有效字符
  2. 判断 判断当前区间大小是否等于p的长度,如果满足,出窗口
  3. 更新结果 判断有效字符的个数是否等于p的长度

代码

class Solution {
    public List<Integer> findAnagrams(String ss, String pp) {
        // 滑动窗口算法
        char[] s = ss.toCharArray(), p = pp.toCharArray();
        int l = 0, r = 0, n = s.length;
        int[] hash1 = new int[26], hash2 = new int[26];
        for(char ch : p) hash2[ch - 'a']++;
        List<Integer> ret = new ArrayList<>();

        int cnt = 0;// 使用cnt统计有效字符的数量
        while(r < n) {
            // 进窗口
            char in = (char)(s[r] - 'a');
            hash1[in]++;
            if(hash1[in] <= hash2[in]) cnt++;// 有效字符

            // 判断 + 出窗口
            if(r - l + 1 > p.length) {
                char out = (char)(s[l] - 'a');
                if(hash1[out] <= hash2[out]) cnt--;
                hash1[out]--;
                ++l;
            }

            // 更新结果
            if(cnt == p.length) ret.add(l);
            ++r;
        }

        return ret;
    }
}

总结
本题判断是否是异位词的策略很巧妙,即使用一个计数变量cnt来标记有效字符的个数,判断区间内部有效字符的个数和字符串p是否相等来判断是否是异位词,此外还有两个比较繁琐的判断策略,这里也提供给大家

  1. 使用hash2统计字符串p中的所有字符及其出现的频率,使用hash1来统计遍历过程中的字符和出现的频率,当区间长度相等时,判断两个哈希表是否相等即可(equals())
  2. 使用数组模拟哈希表,具体策略和1类似,在判断是否相等时可使用循环遍历判断两个数组是否相等

7.串联所有单词的⼦串(分组 + 滑动窗口)

串联所有单词的⼦串
在这里插入图片描述
分析

  • 本题是字母异位词的plus版本,字母异位词中需要判断是否含有某个字符,再遍历的过程中是一个字符一个字符进行判断,本题需要判断的是字符串
  • 算法的思路大致和字母异位词相等,有三点需要注意
  1. 滑动窗口的执行次数:在字母异位词这道题目中,执行一次滑动窗口就能完成,因为是按字符遍历,但是本题是按字符串遍历,需要找到字符串起始字符的位置,有效字符的起始位置不一定就是0位置,也有可能是1,2,…位置,但是最多等于words[i].length - 1,所以需要执行words[i].length - 1次滑动窗口算法
  2. 哈希表的存储:字母异位词中使用哈希数组模拟哈希表,因为都是小写的字符;本题只能使用哈希表还存储字符串和其出现的频率
  3. l和r指针的移动步数:本题是按字符串遍历,所以指针一次移动的步数等于words[i].length - 1

代码

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<>();
        int m = words.length, n = words[0].length(), len = s.length();
        if(len < m * n) return ret;

        Map<String, Integer> hash1 = new HashMap<>();
        for(String str : words) hash1.put(str, hash1.getOrDefault(str, 0) + 1);

        for(int i = 0; i < n; i++) {
            int l = i, r = i, cnt = 0;
            Map<String, Integer> hash2 = new HashMap<>();
            while(r + n <= len) {// 等于len的时候也有可能满足条件  下面唯一会发生的越界的就是第一行代码  是左闭右开
                // 进窗口
                String in = s.substring(r, r + n);
                hash2.put(in, hash2.getOrDefault(in, 0) + 1);
                if(hash2.get(in) <= hash1.getOrDefault(in, 0)) cnt++;// 有效字符

                // 判断 + 出窗口
                if(r - l + 1 > m * n) {
                    String out = s.substring(l, l + n);
                    if(hash2.get(out) <= hash1.getOrDefault(out, 0)) cnt--;// 删除的是有效字符
                    hash2.put(out, hash2.get(out) - 1);
                    l += n;
                }

                // 更新结果
                if(cnt == m) ret.add(l);
                r += n;
            }
        }

        return ret;
    }
}

8.最小覆盖子串

最小覆盖子串
在这里插入图片描述
分析

  • 本题的解法思路和上题类似
  • 需要注意本题求的是最小覆盖子串,求的是最小长度,需要在条件判断成立是更新结果

代码
哈希表解法

class Solution {
    public String minWindow(String ss, String tt) {
        char[] s = ss.toCharArray(), t = tt.toCharArray();
        Map<Character, Integer> hash1 = new HashMap<>();
        Map<Character, Integer> hash2 = new HashMap<>();
        for(char ch : t) hash2.put(ch, hash2.getOrDefault(ch, 0) + 1);

        String ret = "";
        int l = 0, r = 0, n = s.length, len = t.length, minlen = 0x3f3f3f3f, cnt = 0;
        if(n < len) return ret;

        while(r < n) {
            // 进窗口
            char in = s[r];
            hash1.put(in, hash1.getOrDefault(in, 0) + 1);
            if(hash1.get(in) <= hash2.getOrDefault(in, 0)) cnt++;

            // 判断 + 出窗口 + 更新结果
            while(cnt == len) {
                if(r - l + 1 < minlen) {
                    ret = ss.substring(l, r + 1);
                    minlen = r - l + 1;
                }
                char out = s[l];
                if(hash1.get(out) <= hash2.getOrDefault(out, 0)) cnt--;
                hash1.put(out, hash1.get(out) - 1);
                ++l;
            }

            ++r;
        }

        return ret;
    }
}

数组解法

  • s和t中的元素都是英文字母,ASCII码值为97-122,可以开辟一个大小为128的数组(128是ASCII码的最大值)
class Solution {
    public String minWindow(String ss, String tt) {
        char[] s = ss.toCharArray(), t = tt.toCharArray();
        int[] hash1 = new int[128], hash2 = new int[128];
        for(char ch : t) ++hash2[ch];

        String ret = "";
        int l = 0, r = 0, n = s.length, len = t.length, minlen = 0x3f3f3f3f, cnt = 0;
        if(n < len) return ret;

        while(r < n) {
            // 进窗口
            ++hash1[s[r]];
            if(hash1[s[r]] <= hash2[s[r]]) cnt++;

            // 判断 + 出窗口 + 更新结果
            while(cnt == len) {
                if(r - l + 1 < minlen) {
                    ret = ss.substring(l, r + 1);
                    minlen = r - l + 1;
                }
                if(hash1[s[l]] <= hash2[s[l]]) cnt--;
                hash1[s[l++]]--;
            }

            ++r;
        }

        return ret;
    }
}
  • 所谓的算法优化都是建立在暴力解法的基础之上,正是看到了暴力解法的冗余,才想到优化的算法

  • 滑动窗口算法有一个比较明显的切入点求区间内部最长/最短问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值