滑动窗口专题

滑动窗口解决字符串问题,一般都是map集合与滑动窗口相配合,这里的map集合主要用来判重,将字符本身作为键值,map集合key保持唯一性,利用containsKey方法在字符已经存在在map中时对值进行操作,一定程度上也能反应原本字符串本身的情况,这里滑动窗口大小就要根据题目条件自行来设置逻辑规则了,一般情况下都是保留左右两个指针,右指针一直往前走,左指针根据题目要求条件卡到某一位置,r - l +1 即滑动窗口的大小也是符合题目要求的字符串个数,另外一般最后的返回结果都是用Math.max,这样第一个参数可以保留上次结果

一.无重复字符的最大字串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length()==0) return 0;
        HashMap<Character, Integer> map = new HashMap<Character, Integer>();
        int res = 0;
        int left = 0;
        for(int i = 0; i < s.length(); i ++){
            if(map.containsKey(s.charAt(i))){
                left = Math.max(left,map.get(s.charAt(i)) + 1);
            }
            map.put(s.charAt(i),i);
            res = Math.max(res,i-left+1);
        }
        return res;
        
    }
}

主要用到滑动窗口的思想,按序遍历原字符串加入到map中,正常套路没有重复不进if条件时用左减右+1,倘若map中包含了此字符,左边界往右抬一下,代表你还是认可上一次的结果。
将左边界往右抬一下是利用left = Math.max(left,map.get(s.charAt(i)) + 1);代码实现的,这个代码的巧妙之处在于map集合中字符作为键其序号作为值,那么将获取到的序号+1再取max自然而然就相当于++left了,最终就可以卡一个无重复元素的滑动窗口res了。

二.至多包含两个不同字符的最长子串

给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t ,并返回该子串的长度。

示例 1:

输入: "eceba"
输出: 3
解释: t 是 "ece",长度为3。

示例 2:

输入: "ccaabbb"
输出: 5
解释: t 是 "aabbb",长度为5。
class Solution {
    public int lengthOfLongestSubstringTwoDistinct(String s) {
        Map<Character, Integer> map = new HashMap<>();
        int res = 0;
        for(int l = 0, r = 0; r < s.length(); r++) {
            char c = s.charAt(r);
            if(!map.containsKey(c)) {
                map.put(c, 1);
            }else {
                map.put(c, map.get(c) + 1);
            }
            
            while(map.size() > 2) {
                char leftChar = s.charAt(l++);
                int value = map.get(leftChar) - 1;
                if(value == 0) {
                    map.remove(leftChar);
                }else {
                    map.put(leftChar, value);
                }
            }
            res = Math.max(res, r - l + 1);
        }        

        return res;
    }
}

相比上一题我觉得这个更有难度,此题应该卡一个字符种类不超过两种的滑动窗口,一样借用map,将字符串顺序遍历放入map集合中,用map的containsKey方法可以判断集中是否存有该字符,利用map集合key相同会覆盖值的特性可以对其值进行累加代表该字符在字符串中出现的次数,也正是因为map集合key的唯一性的保证,使得map.size>2 时即现在扫描到了超过两种不同的字符。
在代码中窗口的大小是靠res维持的,在正常情况下不进入while循环res就是老套路右减左+1,当map.size第一次大于2时即此时对于原字符串扫描到了三种字符,此字符串不满足题目要求,必须维持上一次的扫描结果,很简单,把左边界往右抬一个就行,res就会维持不变,但这只是你从抽象意义上将窗口大小维持的与上一次一样,也就是从逻辑上你目前认可的是上一次的字符串结果,但是右指针在不断往前走,下一次扫描到不同字符依然会触发map.size>2你继续这样巧妙将res维持不变没错,倘若是连着两次甚至三次又扫描到了不同的相同字符,比如示例中的ccaabbb,连着两次扫描到bb则cc可能就不具有参考意义了。
因而每次在while循环中对左边界left对应的字符值-1,首先left边界是靠原字符串去定位的,而操作是对map中的该字符-1,这样做的意义就是解决上述问题,彻底的从逻辑上卡死原字符串的左边界,比如在连续三次扫描到b后,c对应的值会减为0从map中移除,也就是你从逻辑上认为res区间不再是ccaa而是aabbb,如果后面还有字符再次加入map时之前的c就不具有参考性了,将左边界彻底卡到了a上。

三.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int res = 0;
        int left = 0, right = 0;
        int sum = 0;
        while (end < n) {
            sum += nums[right];
            while (sum >= s) {
                res = Math.min(res,right - left + 1);
                sum -= nums[left];
                left++;
            }
            right++;
        }
        return res;
    }
}

维持一个窗口,让窗口内的数可以最少并且和大于题目要求target,老规矩左右两个指针,正常情况下我让右指针不断去往前遍历数组斌且用sum记录下每次的和,滑动窗口需要动脑子的永远是对于左边界的处理。
根据题目要求,即当sum累加到大于target时就要对左边界处理,在代码中是通过内层while处理的,首先用res记录下当前已扫描到的数组长度,即当前sum对应的数组长度,这很容易,右减左+1。
接下来让sum剔除掉左边界对应的数值,并把左边界往右抬一下,如果剔除掉左边界值sum变小小于了target,那就不会再次进入内层while循环,right++,此时就代表我将窗口整个前移了一格,伴随着下一次进入while新的sum和也会计算出来,但是值得注意的是此时的res从逻辑上讲依然是窗口没前移的状态,只是恰巧跟你前移一格是一样的,因为它代表的是窗口长度;
**这样处理有一个好处就是避免跳过左边最合适的长度,**因为让若sum变大了你就抬左边界,你把窗口左边界往右卡了之后,那万一后面的都是小数要凑好几个才能凑够target那你不就白白把之前左边合适的长度跳过去了吗
因而你窗口整个前移了代表数大了,在内层while操作了一遍,此时的res从逻辑上讲保留的是上次的状态
如果窗口整个前移后剔除掉左边界后的sum小于了target并且后面都是小数不够再次触发内层while,那等right扫描完自然而然返回的就是之前那个res,你但凡想要再次改变res你就得进入内层while,而内层while再次保留res时会与之前那个res取min
最终res一定是最合适的那个窗口大小。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值