2021新年算法小专题—1.滑动窗口(Java)

概述

滑动窗口这个方法可以更低的时间复杂度解决一些比较复杂的字符串问题,因为传统解决字符串问题时都是多次扫描,而滑动窗口只从头至尾扫描一次,在这个过程中维护一个窗口,把符合题意的子串限定在这个窗口中,走过一次就能得出答案。那么如何定义这样一个窗口,窗口如何进行更新呢?

窗口的定义可以借助两个指针,左边界和右边界指针,通常我们取左闭右开,刚开始不明白为什么这样取,写今天的笔记突然想到一个点,大家可以往后面看,我会说到。就以两个指针i、j分别代表左右端点,初始时i、j都取0,也就是刚开始窗口大小是0(因为是[i,j)),然后窗口会先扩大(右边界右移),当满足题目条件时停下(不再扩大),并且开始收缩(左边界右移)。这里我们发现在操作中始终都是左右边界i、j向右移动,也就是不会走回头路(一旦i向左移动了其实就是回溯了,又回去之前走过的路了),因此我们的时间复杂度能保持在O(n)

下面以图来直观说明窗口滑动的过程。

首先,初始时我们的窗口两个指针i、j为0,窗口大小size为0。

image-20210210220236551

然后向前滑动,即j不断+1,i不动,直到窗口内的元素满足题意时停止。这里假设到下图位置时,窗口中元素满足题意了。此时左边界指针i仍然是0,右边界指针j为6,窗口大小是5。这5个元素中就包含我们要的答案。

image-20210210222359022

到此窗口扩大的部分就完成了,接下来就要收缩窗口了。为何要收缩窗口呢?因为我们刚才在移动时并没有对左边界进行移动,所以会导致我们的窗口并不是题中要求的最小的解,因此我们要把左边界不断右移,直到不再满足题意。每次移动我们都记录最优解,当i移动到使窗口不再满足题意时,继续重复刚才的步骤,扩大窗口(右边界j右移,左边界i不动)。

如下图,假设此时仍然满足题意,记录下此时的最优解,然后左边界i继续尝试右移。

image-20210210224323259

移动到下图的位置,通过判断发现现在窗口中的元素不再满足题意了,此时就要开始重新扩大窗口了,重复上面的过程,直到右边界j到达字符串的末尾位置。

image-20210210231421178

最后根据窗口滑动过程中产生的最优解进行处理即可。

总结

下面总结一下滑动窗口类问题的解题思路:

  1. 确定在谁上进行滑动。
  2. 确定窗口中的元素需要满足的条件,用合适的数据结构去记录窗口中当前的满足情况,以及最优解如何记录。
  3. 进行滑动,先扩大(j++),再缩小(i++),永不回退,缩小到不满足题意再继续扩大,直到j走到端点。
  4. 输出最优解。

题目

LetCode中有如下滑动窗口类题目:(欢迎补充,评论贴出题号,我会完善的~)

难度:简单

剑指57-II.和为s的连续正数序列

难度:中等

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

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

567.字符串的排列

难度:困难

76.最小覆盖子串

239.滑动窗口最大值

480.滑动窗口中位数

例题

本文中先以76题最小覆盖子串为例,来感受一下滑动窗口的思路,在后续刷题的文章中会继续分析更多该类问题,到时会贴上链接。

题目

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

**注意:**如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 :

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
分析

题意为在s中找到包含t中全部字母的一个最短的子串。这就是典型的滑动窗口类问题。我们可以在s上进行滑动,维护一个窗口,记录窗口中当前具有哪些满足t的字母,同时考虑到字母可能会重复,因此我们应该记录每个字母及其个数,这样很自然想到应该用集合类存储,我使用HashMap来存储字母及个数。

具体的,使用两个集合needwindow,其中need维护t中的各个字母及个数,为所需字母集合,key为字母,value为对应的个数;window维护当前窗口中各个字母及个数,keyvalue同上。

实现的第一步就是把t的要求写到need集合中,后面都是根据这个集合的值去判断窗口怎么移动。

接下来就初始化两个指针i、j,一个变量valid记录满足t所需字母的个数,当window中的字母满足了t中一个字母的个数要求(如t中含有2个a,window中也含有2个才算),valid就加一,当valid等于t中字母的个数(不同的字母,也就是need集合的size),就代表当前窗口已经满足要求了,是一个解(但不一定是最优解)。另外还需要两个变量记录最佳子串的位置信息,start记录起始坐标,len记录长度,因为我们要找的最优解其实就是最短的子串,因此在比较最优时比较的就是子串的长度,所以len初始值设为int_max

万事俱备,然后就是while循环进行滑动了,退出条件就是j到达字符串s的端点。在循环中每次都移入一个字母(j++),然后判断移入的这个字母是否是t中需要的,如果是,把window集合中该字母的value值加一(没有则创建,具体实现见代码)。接下来判断当前窗口是否已经满足了t串的要求,即need集合的大小等于valid,这时就开始收缩窗口,收缩窗口中也是while循环,退出条件是window中的字母不再满足need的需求。在收缩窗口的循环中,每次都移出一个字母(i++),同时也要对windowvalid做修改。当然最优解就是在收缩窗口的阶段产生的,因此在这个阶段我们还要根据window去更新最短子串的信息startlen

经过以上的循环,最后的最短子串就是s中以start为起点,长度为len的子串。求String的子串使用substring方法,而该方法是左闭右开的,即s.substring(1,2)实际上只有一个元素,这也是我认为滑动窗口也统一为左闭右开的原因之一:统一

设置为左闭右开事实上还有一个原因,就是方便计算窗口的长度,我们以下标从0开始为例,0 1 2 3 4这样一个序列,如果i在0位置,j在3位置,实际上窗口中有0、1、2三个元素,长度为3,直接用j-i就可以得到了,而不是像双闭区间求长度还需要再+1。反正是为了方便~

代码
class Solution {
    public String minWindow(String s, String t) {

        HashMap<Character,Integer> need=new HashMap<>();// 所需的字母集合,key为字母,val为个数
        HashMap<Character,Integer> window=new HashMap<>();// 当前窗口中的字母集合,key为字母,val为对应的个数
        for(int i=0;i<t.length();i++){
            // 如果之前不存在这个key,就设为1,存在就在原来基础上加一
            need.put(t.charAt(i),need.getOrDefault(t.charAt(i),0)+1);
        }
        int left=0,right=0;//初始窗口的左右边界 (左闭右开,所以初始是没有值的)

        int valid=0;// 记录满足所需字母的个数,当这个个数等于t.len的时候说明集齐了

        int start=0,len=Integer.MAX_VALUE;//记录匹配的最小子串信息(起始坐标、串长度)

        while(right<s.length()){

            char cur=s.charAt(right);// 本次移入的字母
            right++;// 窗口右移

            if(need.containsKey(cur)){// 如果本次移入的字母正是需要的
                window.put(cur,window.getOrDefault(cur,0)+1);
                if(need.get(cur).equals(window.get(cur))) valid++;// 这个字母的数量已经满足了,匹配数+1
            }

            while(valid==need.size()){// 当窗口中相应字母全部满足要求了,就开始收缩窗口
                if(right-left<len){// 如果当前窗口长度比 当前最小子串小 就更新
                    start=left;len=right-left;
                }
                
                char delete=s.charAt(left);
                left++;// 窗口左移 移出delete这个值
                
                if(need.containsKey(delete)){// 如果要移除的字母是所需的字母
                    // 如果窗口中这个字母的数量 现在是等于所需数量的,那么移除后就不再满足该数量了,匹配数-1
                    if(window.get(delete).equals(need.get(delete)))  valid--;

                    window.put(delete,window.get(delete)-1);// 窗口中该字母的个数-1
                }
            }
        }
        return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
    }
}

要注意的问题:

  1. hashmap的比较要用equals,而不能用==,因为Integer是引用类型,会造成判断不准确。
  2. 如果len最后仍然是我们赋的初值int_max,说明没有成功进行过一次缩小窗口的操作,即无解,所以返回空串""即可。

参考

  1. labuladong的算法小抄,链接
  2. nettee在letcode上的题解,链接

后记

不知不觉就写到12点多了quq,果然第一天没有完成这个任务…好吧,这就是新年第一篇小文章了!时间还是比较仓促,后续会对这个例题加一些图解,完善的。睡醒继续解一些滑动窗口的题目来更新。大家新年快乐!晚安啦~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
滑动窗口算法(Sliding Window Algorithm)是一种常用的算法技巧,用于解决一些数组或字符串相关的问题。它通过维护一个固定大小的窗口,并在窗口滑动的过程中对窗口中的元素进行处理,从而得到问题的解。 下面是一个使用滑动窗口算法解决问题的示例,使用Java语言实现: ```java public class SlidingWindowAlgorithm { public static void main(String[] args) { int[] nums = {2, 4, 1, 5, 3, 2, 7, 1}; int targetSum = 8; int result = findTargetSum(nums, targetSum); System.out.println("Result: " + result); } public static int findTargetSum(int[] nums, int targetSum) { int windowSum = 0; // 窗口内的元素和 int windowStart = 0; // 窗口的起始位置 int minLength = Integer.MAX_VALUE; // 记录最小子数组长度 for (int windowEnd = 0; windowEnd < nums.length; windowEnd++) { // 窗口右移,加上当前元素 windowSum += nums[windowEnd]; // 当窗口内元素和大于等于目标和时,缩小窗口 while (windowSum >= targetSum) { // 更新最小子数组长度 minLength = Math.min(minLength, windowEnd - windowStart + 1); // 窗口左移,减去左边界元素 windowSum -= nums[windowStart]; windowStart++; } } return minLength != Integer.MAX_VALUE ? minLength : 0; } } ``` 以上示例中,我们使用滑动窗口算法来寻找数组中和大于等于目标和的最小子数组长度。通过维护一个窗口,不断向右移动,并根据窗口内元素和与目标和的比较来调整窗口的大小。最后返回最小子数组长度。 希望以上示例能帮助到您理解滑动窗口算法的使用。如果您有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值