【笑着写算法系列】学会滑动窗口只需七道题

时间:2025-01-17 11:56:24

一、长度最小的子数组

【笑着写算法系列】学会滑动窗口只需七道题_数组

题目要求:求和大于target的长度最小的连续子数组。


代码及其解析:

我们第一层while循环是进窗口,当大于target时进入第二层循环进行更新结果,在出窗口,然后继续进窗口,这样我们就能把一个窗口不断往右移了。这是最基础的滑动窗口了

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int size=nums.length;
        int left=0;
        int right=0;
        int sum=0;
        int len=99999999;
        while(right<size){
              sum+=nums[right];

          

            while(sum>=target){
            len=Math.min(len,(right-left+1));

             sum-=nums[left];
             left++;
            }

            right++;

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

二、无重复字符的最长字串

【笑着写算法系列】学会滑动窗口只需七道题_字符串_02

题目要求:就是给出一段字符串,要求求出最长的连续子串,其中子串不能有重复的元素。

字符串有数字,字母,空格组成。

代码及其解析:

首先我们先将字符串转化为一个arr数组,然后在创建一个hash数组,模拟一个哈希表,用于检查当前选定字符是否已经被包含在该段连续字符串。

即:检查left与right-1之间是否包含了当前right字符。

然后套一个while循环,用于让right不断往后走,直到数组末尾

然后在进窗口的while循环,当我们当前字符串在hash中不存在时,即:hash的值是0,我们进窗口,否则出窗口

然后更新结果,在将left移到与arr[right]相等的位置,然后继续将窗口往右滑。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char[] arr=s.toCharArray();

        int[] hash=new int[128];
        int left=0;
        int right=0;
        int count=0;

        while(right<arr.length){
            int c=arr[right];


            while(hash[c]==0){

                hash[c]++;
                right++;

                if(right>=arr.length) break;
                c=arr[right];

            }
            count=Math.max(count,right-left);

            while(right<arr.length && arr[left]!=arr[right]){
                hash[arr[left]]--;
                left++;
            }

            left++;
            right++;

        }

        return count;


    }
}

三、最大连续1的个数

【笑着写算法系列】学会滑动窗口只需七道题_hash表_03

题目要求:就是在一段有数字0和1组成的数组中,我们可以有把k次把0变成1的机会,让我们找出最长连续1的个数,例子如图所示。


代码及其解析:

首先看题目,连续,且子数组,很容易就联想到滑动窗口,然后我们在看看滑动窗口的条件

对于这个能否使用滑动窗口的条件我是这样理解的:当我们把right加进窗口时,我们的目标值(本题中我们指的是count)只有增加或者不变,有亦或是只出现减少和不变,也就是说进窗口后目标值是呈现单调性的。


首先我们定义进窗口的循环,依旧是right<nums.length,我们先检测当前right的值是否为0,如果是0我们就直接count++,count指的就是0的数量,当count>k时我们就可以出窗口了,定义第二个循环

在第二个循环中,我们首先更新结果,然后开始将左边的值出窗口,直到count<=k,这时我们就可以继续进窗口了。需要注意的是我们要在第二层循环中限制 leftt要小于right。



class Solution {
    public int longestOnes(int[] nums, int k) {
        int left=0;
        int right=0;
        int count=0;
        int max=0;
        while(right<nums.length ){
            if(nums[right]==0){
                count++;
            }

            while(count>k && left<=right){
                max=Math.max(max,right-left);

                if(nums[left]==0){
                    count--;
                }
                left++;
            }
            right++;
        }
        max=Math.max(max,right-left);
        return max;
    }
}


四、将x减少到0的最下操作数

【笑着写算法系列】学会滑动窗口只需七道题_字符串_04

题目解析:在一个数组中我们数组的最右端或左端,去掉这个值,并让x减去这个值,不断重复,直到让x=0;

这时我们就需要返回我们将x减成0,我们删除个数最少的一次,是删除了多少个数,即:最少操作数。


代码及其解析:

看到这一道题,如果我们真的按照从左右两边不断减去,暴力方法尝试是否能将x减成0,那太复杂了。

这里我们可以转变一下思路,首先我们先是想想从两边减去值,就只剩下中间连续的数组,那些数不就组成一个窗口了吗,而且这些数的和就等于原数组全部数的和total-x(total要我们自己求出来),那么我们的问题不就是滑动窗口求窗口的值为total-x。

具体代码操作就如下:

首先求出total,如果total==x,直接返回数组长度。

然后开始套第一层进窗口,第二层循环就是出窗口,如果sum==total-x,我们就更新操作数len,其实也就是窗口前面加上窗口后面的元素个数之和。

class Solution {
    public int minOperations(int[] nums, int x) {
        int left=0;
        int right=0;
        int len=99999;
        int sum=0;
        int total=0;

        for(int i=0;i<nums.length;i++){
            total+=nums[i];
        }

        if(total==x){
            return nums.length;
        }

        while(right<nums.length){
            sum+=nums[right];

            while(left<=right&&sum>=total-x){
                if(sum==total-x){
                    len=Math.min(len,left+nums.length-right-1);
                }
                sum-=nums[left++];

            }
            right++;
        }

        return len==99999?-1:len;

    }
}


五、水果成篮

【笑着写算法系列】学会滑动窗口只需七道题_hash表_05

【笑着写算法系列】学会滑动窗口只需七道题_字符串_06

题目解析:再fruits就是求一段只有两种数字元素的最长子数组,这个问题其实与我们上面的连续1的最长子数组挺像的,不同的是这一道题更复杂了点。


代码及其解析:

首先我们需要借助道hash表,来存储我们已经进窗口的值。

然后依然是定义left,right,等等变量,其中count是用来计量窗口中数字种类的个数,当count>2时我们就更新结果并出窗口。

具体为:一层while循环为进窗口,判断hash表中是存储有当前right数字的种类,如果没有count++。然后扔进hash表,当count>2时我们就可以更新结果并出窗口,注意出窗口时,注意判断当前left出去后,hash表中是否还存储有该数字种类,若没有需要及时count--;而不是单纯的hash表key对应的value--而已。

最后注意再返回最后的sum个数时,需再进行一次更新结果,因为有可能count<=2,但是数组已经遍历完了,没有进到第二次循环更新结果。

import java.util.HashMap;

class Solution {
    public int totalFruit(int[] fruits) {
        int sum=0;
        int left=0;
        int right=0;
        int count=0;
        HashMap<Integer,Integer> hash=new HashMap<>();

        while(right<fruits.length){
            int key=fruits[right];
            if(hash.getOrDefault(key,0)==0){
                count++;
            }
            hash.put(key,hash.getOrDefault(key,0)+1);


            while(left<=right&&count>2){
                sum=Math.max(sum,right-left);
                System.out.println(right);
                System.out.println(left);

                key=fruits[left++];

                hash.put(key,hash.get(key)-1);
                if(hash.get(key)==0){
                    count--;
                }
            }
            right++;
        }
        sum=Math.max(sum,right-left);
        return sum;
    }
}

注意:其实代码还可以在优化,就是将hash容器,改成更加轻量级的数组。大家可以去尝试尝试。


六、找到字符中所有的异位词


【笑着写算法系列】学会滑动窗口只需七道题_数组_07

【笑着写算法系列】学会滑动窗口只需七道题_hash表_08

代码及其解析:这里我们继续使用滑动窗口,这里我们需要计数,记住即p中出现各个字母的次数,所以这里我们需要用到hash表,但是我们这一题可以用一个简单的数组来代替,也可以提供点效率。

首先我们先定义一个arr数组充当一个hash表,因为p只包含小写字母,所以我们只需26个容量得了。

然后开始一个一个遍历字符串中的字母存在变量c中,没每遍历一个就在就在arr[c-97]++,这样我们就可以使得p中的字母出现个数在arr中对应的“字母ascll-97”中存储。

然后经典的左右指针开始进窗口,如果在遍历到的字母对应的数组中值不为0,那么很简单直接将arr[c-97]--后

count++,然后右指针继续遍历下一个。麻烦一点的就是arr[c-97]==0的时候,说明这时左右指针间形成的字符串已经不是异位词,这时我们就需要将左指针向移动也算是出窗口了,移动给arr[c-97]++返回个数,且count--

直到当前right指针对应的arr[c-97]不再是0,这时会涉及到一个特殊情况,即left=right+1,count=-1的情况,但最多也就是该字母压根没在p字符串中,这时left一直移动,直到right位置的arr[c-97]++,count--后,来到right+1的位置。这时right在进行一次判断,count++,arr[c-97]--,right++后。left==right相当于重新在这个地方开始。往后遍历,那么我们判断窗口的时机就是在每次count++后,对count==p.length进行判断,如果等于代表窗口内的就是异位词。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int size=p.length();
        List<Integer> list=new ArrayList();
        int[] arr=new int[26];
        char c=0;
        for(int i=0;i<size;i++){
             c=p.charAt(i);
             arr[c-97]++;  
            
        }
        

        int right=0;
        int left=0;
        int count=0;
        while(right<s.length()){
            c=s.charAt(right);

           
            if(arr[c-97]!=0){
                arr[c-97]--;
             
                count++;
            }else{
                c=s.charAt(left);
                arr[c-97]++;
                left++;
                count--;      
               continue;
            }

            if(count==size){
                list.add(left);
               
                c=s.charAt(left);
                
                arr[c-97]++;
                left++;
                count--;     
            }
            right++;
        }
        return list;
         
        
    }
}

这里我们也可以使用两个hash作比较,这种方法就不写出来了,下一题就是用两个hash来解题,可以参考一下。

七、串联所有单次的字串

【笑着写算法系列】学会滑动窗口只需七道题_数组_09

题目解析:与上一题比较相似,就是在这个长字符串s中找出与words这个字符串数组中的字符串,按任意顺序进行拼接而成的字符串相同的子字符串,示例如图所示。


代码及其解析

在这一题中,我们还是需要用到hashmap,而且还是两个,并且不能简单的用数组代替,主要原因就是因为在这里的基本拼接元素是一个字符串而不是像上一题一样只是单个字符。

思路:首先我们要思考到我们一次选取s中的字符串是几个几个的选,那么我们选取的选择就会有几种不同的选法了,比如abcdefg,我要三个三个的选我可以是,abc,def也可以是bcd,efg,也可以只选出一个cde。所以我们就要对着三种情况的每一种都要进行一次选取,然后进窗口,出窗口。这里的三种情况是我举例的,具体选取取决于words中字符串的长度len。

首先我们需要定义第一个hash表,用来存储words中的字符串个数。将words中的字符串全部塞入hash中。便于后面我们判断。


那么我们代码首先第一循环,i代表left,right开始分字符串的位置,往后每len个字符为一个单位进入hash2表中,这时我们需要判断进入hash2中的字符串有没有超过words中出现的频次

if(hash2.get(in)<=hash.getOrDefault(in,0)),如果没有则count++,代表是一个有效的字符串,如果不是我们就无需理会,因为后面窗口往后移动会把这个超出的字符串移除-1的。当我们窗口的字符串个数超出时我们就应该出窗口了。出窗口也是left往后移,然后判断(hash2.get(in)<=hash.getOrDefault(in,0))看看是否count需要减1。最后就是判断count==m(words的length),如果等于就可以更新结果了。注意right+=len的位置必须是在判断right-left+1是否大于m*len,即判断是否出窗口的时候。


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> list=new ArrayList<>();
        int len=words[0].length();
        int left=0;
        int right=0;
        int count=0;
        int m=words.length;


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

        for(int i=0;i<len;i++){

            left=right=i;
            count=0;

            HashMap<String,Integer> hash2=new HashMap<>();

            while(right<=s.length()-len){

                String in=s.substring(right,right+len);
                hash2.put(in,hash2.getOrDefault(in,0)+1);
                if(hash2.get(in)<=hash.getOrDefault(in,0)){
                    count++;
                }

                if(right-left+1>m*len){
                    String out=s.substring(left,left+len);
                    if(hash2.get(out)<=hash.getOrDefault(out,0)){
                        count--;
                    }
                    hash2.put(out,hash2.get(out)-1);

                    left+=len;
                }

                if(count==m) list.add(left);
                right+=len;

            }
        }
        return list;

    }
}


七、最小覆盖字串

【笑着写算法系列】学会滑动窗口只需七道题_数组_10

题目解析:要求我们在长字符串s找出包含t中字符串中所有字符的子串,这里我们也可以使用两个hash来解决。

代码及其解析:

我们还是老样子用数组简单代替hash,我们在数组arr里放进t字符串的字符,然后进窗口放入are1时看看是否是有效字符,即频次有没有小于arr,if(arr1[c-'A']<=arr[c-'A']) count++;如果少于就是有效字符count++。

当我们发现有效字符count==t的长度时,就可以更新结果,然后开始出窗口,并判断出窗口的字符是否是有效字符。我们的结果是存储在ret数组中,ret[0]存储的是left的位置,ret[1]则存放right的位置,最后我们把字符串给截取出来即可。如果len的长度没变还是99999999就代表没有这个子串

class Solution {
    public String minWindow(String s, String t) {
        char[] s1=s.toCharArray();
        char[] t1=t.toCharArray();
        int[] ret=new int[2];

        int len=99999999;
        int[] arr=new int[100];
        int[] arr1=new int[100];
        int count=0;
        for(char a:t1) arr[a-'A']++;
        int left=0,right=0;

        while(right<s1.length){
            char c=s1[right];
            arr1[c-'A']++;
            if(arr1[c-'A']<=arr[c-'A']) count++;
           

            while(count==t1.length){
                
                if(len>right-left+1){
                  len=right-left+1;
                  ret[0]=left;
                  ret[1]=right;
                }
               
                c=s1[left++];
                
                if(arr1[c-'A']<=arr[c-'A']) count--;
                arr1[c-'A']--;
                
            }
           
            right++;
        }
        return len==99999999?"":s.substring(ret[0],ret[1]+1);
    }
}