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

本篇就是第一个小专题的更多题目,我会将前一篇提到的letcode中的题目在这里解答,附上思路及代码,上一篇关于滑动窗口的讲解链接在此—>2021新年算法小专题—1.滑动窗口(Java代码)

1.和为s的连续正数序列(Easy)

题目

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 :

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
分析

显然是在一个正整数序列中维护一个窗口,从1开始滑动,窗口中的数只有在相加和为target的时候才满足题意。需要注意这个序列的终点并不是无穷的,因为题中要求结果至少应该有两个数,而两数相加等于target则要求两个数一个大于target/2,一个小于target/2,所以说我们最多只需要把左边界滑动到target/2的位置就好了。因此确定:while循环的退出条件是i>target/2

因为我们是要统计数字的和,所以这里就不关注滑动窗口的大小size了,而是关注窗口中数字的和sum。在滑动中,我们需要根据sum来进行判断:当sum小于target时,说明窗口内的数字和还不够,因此扩大窗口,右边界指针j加一 ;当sum大于target时,说明窗口内的数字和已经超过要求的值了,因此尝试收缩窗口,左边界指针i加一 ;当sum和target相等时,说明当前窗口内的子串就是我们需要的,使用合适的数据结构(数组)将他们记录下来,记录完成后我们还要继续进行循环,因此要把窗口左指针i加一,使窗口中的数字和小于要求的值,以再下次循环中继续扩大窗口。

参考代码
class Solution {
    public int[][] findContinuousSequence(int target) {

        int i=1,j=1;// 滑动窗口的左右边界,为方便,直接把数值为1的起始位置下标视为1,下标=值
        int sum=0;// 当前窗口中的数字之和
        List<int[]> ans=new ArrayList<>();

        while(i<=target/2){// 窗口滑动的条件
            if(sum<target){// 窗口中数字的和小于需求,扩大窗口(右边界右移)
                sum+=j;
                j++;
            }
            else if(sum>target){// 窗口中数字的和大于需求,收缩窗口(左边界右移)
                sum-=i;
                i++;
            }
            else{// 窗口中数字的和等于需求了,记录下窗口中的各个值
                int []curAns=new int[j-i];// 记录本次窗口的各个值
                for(int k=i;k<j;k++) curAns[k-i]=k;// 循环填入
                ans.add(curAns);// 加入结果集合
                sum-=i;// 本次记录完成,窗口收缩继续判断
                i++;
            }
        }

        return ans.toArray(new int[ans.size()][]);
    }
}

2.滑动窗口最大值(Hard)

题目

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例 :

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值

---------------               -----

[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
分析

本题就是一种训练滑动窗口思想的题目。题目本身的思路比较简单,我们只需要在sum数组中维护一个大小固定为k的窗口,每次移动都让右边界扩大一个数字,左边界收缩个数字即可。每个窗口位置都要记录当前窗口中的最大值。那么如何快速找到不同状态下的窗口中最大值呢?可以使用队列这种数据结构。我们维护一个全局的单调队列queue,保证最大值一直在队首,这样每次取出当前窗口最大值的时间复杂度就是O(1)

现在我们考虑如何在移动窗口的同时更新这个队列queue。首先这个过程应该分为窗口形成之前和窗口形成以后:因为我们从第一个元素开始走,需要走k个元素后才能形成大小为k的窗口;窗口形成后就可以每次移出一个旧的左边界、移入一个新的右边界了。

在窗口形成之前,即填充k个元素到窗口的过程中,每次填进一个元素,如果队列中存在元素,就使用队尾元素与新元素比较:如果新元素比队尾大,就删除这个队尾元素,直到队列为空或者新元素小于等于队尾元素为止,将新元素入队。

在窗口形成后,即窗口滑动的过程中,每次滑动一个位置,就会涉及到两个发生变化的位置:左边界和右边界(在本题中,我们根据题意把窗口设为双闭的,不再是左闭右开了),左边界移出,右边界移入,需要进行两个判断:一个是移出的元素是否在队列queue的队头位置?如果在,要将它移除(因为当前窗口已经不再包含该元素);另一个是移入的元素是否应该入队?进行如上一步(窗口形成前)的判断即可。

每次窗口滑动后我们都把队列queue的队首元素拿出来记录,这就是本次窗口滑动后的最值。

以示例数据为例,整个过程图解如下:

初始时,窗口大小为0,队列为空,开始形成窗口。

image-20210212225427354

将1移入窗口,判断队列为空,将1入队。此时窗口大小为1,不满足窗口大小3,继续扩大。

image-20210212225635847

将3移入窗口,判断队列的队尾元素1比新元素3小,因此移除1,然后队列为空,把3入队。此时窗口大小为2,继续扩大。

image-20210212225811479

将-1移入窗口,判断队列队尾元素3比-1大,因此直接把-1入队。此时窗口大小为3,满足题目要求,本次的最值是队列的队首3,记录下来。然后开始滑动。

image-20210212230009999

第一次滑动,1移出,-3移入。1不在队首,-3比队尾-1小,把-3入队。本次滑动结果如下:

image-20210212234550321

第二次滑动,3移出,5移入。3在队首,因此移出队首3;5比队尾-3大,-3出队,然后队尾-1、3也都小于5,都出队,最后队列为空,5入队。本次滑动结果如下:

image-20210212234838236

第三次滑动,-1移出,3移入。-1不在队首,3比队尾小,因此直接入队。本次滑动结果如下:

image-20210212235046910

第四次滑动,-3移出,6移入。-3不在队首;6大于队尾3,3出队,也大于队尾5,5出队,队列为空,6入队。本次滑动结果如下:

image-20210212235246781

第五次滑动,5移出,7移入。5不在队首,7比队尾6大,6出队,7入队。本次滑动结果如下:

image-20210212235429874

最终答案是:[3,3,5,5,6,7]。(第一次形成窗口时的最值没有画在max数组中)

参考代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 数组为空或者窗口大小为0 返回空数组
        if(nums.length == 0 || k == 0)  return new int[0];

        Deque<Integer> deque = new LinkedList<>();// 单调队列 用于O(1)时间找到最值
        int[] res = new int[nums.length - k + 1];// 最大值结果(max)
        
        // 还未形成窗口时
        for(int i = 0; i < k; i++) { 
            // 如果队尾元素比新元素小,就将队尾出队
            while(!deque.isEmpty() && deque.peekLast() < nums[i])
                deque.removeLast();
            deque.addLast(nums[i]);// 最后要把新元素入队

        }
        res[0] = deque.peekFirst();// 第一个滑动窗口的最大值

        // 形成窗口后
        for(int i = k; i < nums.length; i++) { 
            if(deque.peekFirst() == nums[i - k])// 如果当前队首是要移出的值,就将它出队
                deque.removeFirst();
            
            // 如果队尾元素比新元素小,就将队尾出队(同未形成窗口时的操作)
            while(!deque.isEmpty() && deque.peekLast() < nums[i])
                deque.removeLast();
            deque.addLast(nums[i]);// 最后要把新元素入队

            res[i - k + 1] = deque.peekFirst();//记录本次滑动窗口的最值
        }
        return res;
    }
}

3.字符串的排列(Medium)

题目

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的排列。

换句话说,第一个字符串的排列之一是第二个字符串的子串。

示例 :

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
分析

本题其实跟上一篇讲解滑动窗口文章中的例题(最小覆盖子串)很相似,为了更好理解本题建议回去看一下,传送门在这

在最小覆盖子串中,我们用到了两个集合needwindow分别记录子串中所需要的字母及个数、当前窗口中包含的所需字母及个数。本题中与最小覆盖子串的区别就在于——找到这个子串就停下,返回true。至于题中的子串“排列”,其实和覆盖字串同理,只需要要求字母及出现次数都相同,就能达到子串的排列效果。只不过在覆盖子串中,窗口大小可以比子串大,即允许中间存在其他字母,如"abcd"匹配"acd";而在本题中,窗口必须等于子串,不能有其他字母,如"abcd"匹配"bdac",而不可以是"bdaec"

首先初始化needwindow集合,意义同上述。然后把s1子串的字母及出现次数写入need。左右边界ij初始化为0,同时声明变量valid记录满足的字母数,然后开始while循环滑动。

首先要扩大窗口,j加一,判断新移入的字母是否包含在need集合中,如果是,更新window;如果更新后window的这个字母的个数满足need要求了,valid加一。如果此时窗口大小等于或者大于了s1的长度,就要开始判断满足或者进行收缩了,因为大于s1一定不会有解了,当need中所有字母的个数都满足时,直接返回true,否则就收缩窗口,i加一,判断移出的字母是否在window中,以及是否导致need中这一字母的个数不再满足(valid减一),这些步骤都与覆盖子串题目相同。

参考代码
class Solution {
    public boolean checkInclusion(String s1, String s2) {

        HashMap<Character,Integer> need=new HashMap<>();
        HashMap<Character,Integer> window=new HashMap<>();

        // 把s1的字母及出现个数统计到need中
        for(int i=0;i<s1.length();i++){
            need.put(s1.charAt(i),need.getOrDefault(s1.charAt(i),0)+1);
        }

        int i=0,j=0;// 左右边界
        int valid=0;// 满足的字母数(字母的数量满足)
        while(j<s2.length()){

            // 首先扩大窗口,让字母进来
            char cur=s2.charAt(j);// 本次移入的新字母
            j++;// 移入新字母,右边界+1
            if(need.containsKey(cur)){// 新字母是所需要的,进行window更新
                window.put(cur,window.getOrDefault(cur,0)+1);// window更新
                if(need.get(cur).equals(window.get(cur))) valid++;// 匹配上一个字母了(某字母数量满足),匹配的字母数加一
            }

            // 判断当窗口大小 大于等于匹配串的大小,收缩窗口
            while(j-i>=s1.length()){
                if(valid==need.size()) return true;// 如果匹配的字母数等于所需字母数了,说明成功匹配到了,返回true
                char del=s2.charAt(i);// 要移出的元素
                if(window.containsKey(del)){
                    if(need.get(del).equals(window.get(del))) valid--;// 如果当前该字母数量已经满足要求,那么删除后就不再满足了,因此valid要减一
                    window.put(del,window.get(del)-1);// 窗口中该元素数量减一
                }
                i++;// 收缩窗口--左边界+1
            }
        }
        return false;
    }
}

4.找到字符串中所有字母异位词(Medium)

题目

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:

  • 字母异位词指字母相同,但排列不同的字符串。
  • 不考虑答案输出的顺序。

示例 :

输入:
s: "cbaebabacd" p: "abc"

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
分析

本题和上题(字符串的排列)十分相似,仍然是在一个串s中找到子串p的排列,返回匹配成功时的起始索引(左边界i)。题目的叙述有一些问题,实际与子串p字母顺序相同的子串也是答案(如原题中示例2:输入s:"abab" p:"ab",输出[0,1,2])。

本题需要额外的一个集合ans来记录匹配成功的答案。其他基本同上题,见参考代码。

参考代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        // 记录字母的出现次数 在need集合中
        HashMap<Character,Integer> need=new HashMap<>();
        HashMap<Character,Integer> window=new HashMap<>();
        for(int i=0;i<p.length();i++){
            need.put(p.charAt(i),need.getOrDefault(p.charAt(i),0)+1);
        }

        int i=0,j=0;
        int valid=0;
        List<Integer> ans=new ArrayList<>();

        while(j<s.length()){
            // 扩大窗口
            char cur=s.charAt(j);
            j++;
            if(need.containsKey(cur)){
                window.put(cur,window.getOrDefault(cur,0)+1);
                if(need.get(cur).equals(window.get(cur))){
                    valid++;
                }
            }

            while((j-i)>=p.length()){// 判断相等及收缩窗口

                if(valid==need.size()){
                    ans.add(i);
                }
                
                
                char del=s.charAt(i);
                i++;
                if(window.containsKey(del)){
                    
                    if(need.get(del).equals(window.get(del))){
                        valid--;
                    }
                    window.put(del,window.get(del)-1);
                }
            }
        }

        return ans;
    }
}

5.无重复字符的最长子串(Medium)

题目

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

示例 :

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

本题比较简单,我们只需保证窗口window中的每个字母只能出现一次即可。具体为:从i=0j=0时开始滑动(while循环),滑动(循环)条件是右边界j<s.length()。先扩大窗口(j++),将本次新进入窗口的字母放入window中,然后判断这个字母在window中的出现次数是否大于1,如果大于1就要收缩窗口以去除重复的字母,使用while循环,直到这个字母在window中的出现次数为1时停止。

经过上面的处理后就能保证当前窗口中所有元素都只会出现一次了(每次新进入window的字母都这样去判断,进来一个判断一个,最终能保证窗口中元素都不重复),每次滑动后都判断本次窗口大小是否是更大的,记录下这个最大值maxLength即可。

参考代码
class Solution {
    public int lengthOfLongestSubstring(String s) {

        HashMap<Character,Integer> window=new HashMap<>();// 记录当前窗口中字母(key)及出现次数(val)

        int i=0,j=0;// 左右边界
        int maxLength=0;// 最大不重复子串长度

        // 滑动
        while(j<s.length()){

            char cur=s.charAt(j);// 新移入的元素
            j++;// 窗口扩大

            window.put(cur,window.getOrDefault(cur,0)+1);// 新元素记录在window中

            // 当新元素的val大于1(含有多个)时收缩窗口 直到只含一个(多余的该字母被移出窗口)
            while(window.get(cur)>1){
                char del=s.charAt(i);// 要移出的元素
                i++;// 窗口缩小

                window.put(del,window.get(del)-1);// 更新window
            }

            maxLength=Math.max(maxLength,j-i);// 更新最值
        }
        return maxLength;
    }
}

更多

更多题目有待后续更新,喜欢的话点个赞或收藏吧
感谢您的喜欢 ~ 祝新年快乐呀 ~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值