leetcode刷题笔记之滑动窗口法

149 阅读4分钟

先看例题:

209. 长度最小的子数组(难度中等)

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

class Solution {    
    public int minSubArrayLen(int s, int[] nums) {        
    int left=0,right=0,length=nums.length+1;        
        while(right<nums.length){            
            //移动右边界            
            while(right<nums.length){                
                int sum=countLR(nums,left,right);                
                if(sum>=s) {                    
                    int temp=right-left+1;                    
                    if(temp<length){                        
                    length=temp;                    
                    }                    
                break;                
                }                
                if(right==nums.length-1)break;                
                right++;            
            }                        
            while(left<=right){                
                int sum=countLR(nums,left,right);                
                if(sum<s)break;                
                int temp=right-left+1;                
                if(temp<length){                    
                    length=temp;                
                }                
                left++;            
            }            
            if(right==nums.length-1)break;        
        }        
        if(length==nums.length+1) return 0;        
        return length;    
    }    
    public int countLR(int[] nums,int left,int right){        
        int sum=0;        
        for(int i=left;i<=right;i++){             
            sum+=nums[i];           
        }        
        return sum;    
    }
}
执行用时:316 ms, 在所有 Java 提交中击败了5.01%的用户
内存消耗:39.7 MB, 在所有 Java 提交中击败了78.80%的用户

因为有for循环(我也不知道我写的时候是怎么想的)

优化一:设置全局变量增减

class Solution {    
    public int minSubArrayLen(int s, int[] nums) {        
        if(nums.length==0)return 0;        
        int left=0,right=0,ength=nums.length+1,int sum=nums[0];        
        while(right<nums.length){            
            //移动右边界            
            while(right<nums.length){                
                if(sum>=s) {                    
                    int temp=right-left+1;                    
                    length=temp<length? temp:length;                   
                    break;
                } 
                if(right==nums.length-1)break;
                right++;
                sum+=nums[right];
            } 
            while(left<=right){
                if(sum<s)break;
                int temp=right-left+1;
                if(temp<length){
                    length=temp;
                } 
                left++;
                sum-=nums[left-1];
            } 
            if(right==nums.length-1)break;
        }        
        if(length==nums.length+1) return 0;
        eturn length;    
    }
}

时间复杂度:O(n),空间复杂度O(1)?

执行用时:2 ms, 在所有 Java 提交中击败了86.87%的用户

内存消耗:39.6 MB, 在所有 Java 提交中击败了89.54%的用户

优化二;改成正则表达式(和if一样的耗时)

Math.min计算时间比较长。

方法二:用栈来代替滑动窗口

class Solution {    
    public int minSubArrayLen(int s, int[] nums) {        
        if(nums.length==0) return 0;        
        Queue<Integer> queue=new LinkedList<>();        
        int i=0,sum=nums[0],length=nums.length+1,temp=length;        
        queue.add(nums[i]);        
        while(queue.size()<=nums.length){            
            if(sum>=s){                
                temp=queue.size();                
                length=temp<length? temp:length;                
                sum-=queue.poll();            
            }else{                
                if(i>=nums.length-1) break;                
                i++;                
                queue.add(nums[i]);                
                sum+=nums[i];            
            }        
        }        
        if(length==nums.length+1) return 0;        
        return length;
   }
}
执行用时:7 ms, 在所有 Java 提交中击败了23.30%的用户
内存消耗:40.5 MB, 在所有 Java 提交中击败了5.55%的用户

优化一:他人代码

改进的地方:

1.min用的是常数

2.在left和right改变的时候计算

class Solution{    
    public int minSubArrayLen(int s, int[] nums) {        
        int lo = 0, hi = 0, sum = 0, min = Integer.MAX_VALUE;        
        while (hi < nums.length) {            
            sum += nums[hi++];            
            while (sum >= s) {                
                min = Math.min(min, hi - lo);                
                sum -= nums[lo++];            
            }        
        }        
        return min == Integer.MAX_VALUE ? 0 : min;    
    }   
}

例题2: 剑指 Offer 59 - I. 滑动窗口的最大值

class Solution {    
    public int[] maxSlidingWindow(int[] nums, int k) {        
        if(nums.length==0) return nums;        
        int left=0,right=k;        
        int[] result=new int[nums.length-k+1];        
        int max=nums[0],maxPos=0;        
        int index=0;        
        while(right<=nums.length){            
            if(maxPos<left || left==0){                
            int[] temp=findMax(nums,left,right);                
            max=temp[0];                
            maxPos=temp[1];            
            }else{                         
                if(max<nums[right-1]){                    
                max=nums[right-1];                    
                maxPos=right-1;                
                }                  
            }            
            result[index]=max;            
            left++;            
            right++;            
            index++;        
        }        
        return result;    
    }    
    public int[] findMax(int[] nums,int left,int right){        
        int[] temp =new int[2];        
        int max=nums[left];        
        int maxPos=left;        
        for(int i=left;i< right;i++){            
            if(max<nums[i]){                
                max=nums[i];                
                maxPos=i;            
            }        
        }        
        temp[0]=max;        
        temp[1]=maxPos;        
        return temp;    
    }
}

时间复杂度:

空间复杂度:

执行用时:3 ms, 在所有 Java 提交中击败了90.83%的用户
内存消耗:47.8 MB, 在所有 Java 提交中击败了78.65%的用户

例题3:剑指 Offer 42. 连续子数组的最大和

这题可以用滑动窗口法做,但效率没有用动态规划做高。

class Solution {
    public int maxSubArray(int[] nums) {            
        if(nums.length==0) return 0;            
        if(nums.length==1) return nums[0];            
        int left=0,right=0;                 
        int max=nums[0],temp=nums[0];            
        while(right<nums.length){                
            if(right!=0)temp+=nums[right];                                
            if(temp<0 && temp<max){                    
            left=right;                    
            temp=nums[right];                    
            max=temp>max? temp:max;                
            }                
            while(right<nums.length-1){                        
                if(nums[right+1]<0) break;                    
                right++;                    
                temp+=nums[right];                   
            }                
            max=temp>max? temp:max;                
            while(left<right){                    
                if(nums[left]>0) break;                    
                temp-=nums[left];                    
                left++;                
            }                
            max=temp>max? temp:max;                               
            right++;                            
       }            
       return max;        
    }
}

这题在leetcode上面的效率很低(打败15%),我挺疑惑的,明明是O(n)的时间复杂度为什么会这么低?

滑动窗口法思路:

左边指针移动条件:指向位置的数<0则移动;

右边指针移动条件:后面一个数>0则移动;

其余情况:如果窗口内和小于0且小于现有和最大值,左右指针都指向窗口右边的位置,因为继续往后这部分的和只会成为拖累,干脆都不要。不用担心如果right指向位置为正数怎么办,如果是正数,就会在之前就淘汰这部分。

if(temp<0 && temp<max){                    
    left=right;                    
    temp=nums[right];                    
    max=temp>max? temp:max;                
}     

例题4:至少有K个重复字符的最长子串

class Solution {
    public int longestSubstring(String s, int k) {
        if (s == null || s.length() == 0) {
            return 0;
        }

        // 使用数组存储,比起hashmap效率更高
        // 若包含大写字母,可以用128,如果包含其他ASCII码,可用256
        int[] hash = new int[26];
        for (int i = 0; i < s.length(); i++) {
            hash[s.charAt(i) - 'a']++;
        }

        // 递归结束条件
        // 判断是否整个字符串都满足条件
        boolean fullString = true;        
        for (int i = 0; i < s.length(); i++) {
            // 若有字母小于 k 个,则说明整个字符串不符合,需要拆开来判断
            if (hash[s.charAt(i) - 'a'] > 0 && hash[s.charAt(i) - 'a'] < k) {
                fullString = false;
            }
        }
        if (fullString == true) {
            return s.length();
        }

        // 滑动窗口
        // right
        int left = 0;
        int right = 0;
        int max = 0;
        while (right < s.length()) {
            // 如果遇到 right 所指元素个数小于 k,则需要由此拆开来比较其他位置
            if (hash[s.charAt(right) - 'a'] < k) {
                max = Math.max(max, longestSubstring(s.substring(left, right), k));
                left = right + 1;
            }
            right++;
        }
        // aaabcccc
        //     l   r  即取到的是 cccc
        max = Math.max(max, longestSubstring(s.substring(left,s.length()), k));
        return max;
    }
}

作者:Jasion_han
链接:https://leetcode-cn.com/problems/longest-substring-with-at-least-k-repeating-characters/solution/395-zhi-shao-you-kge-zhong-fu-zi-fu-de-zui-chang-5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这题比较难,我自己的做法很复杂,看解题思路找到的:hash+滑动窗口+递归。在原本代码上加了注释。

思路:先把找出大字符串中满足所有字母都出现k次的子字符串(必要不充分条件),然后判断整个子字符串是否满足在该字符串里面也做到每个字符出现k次

解法里的递归:

1.边界条件:当字符串本书满足所有字母都出现k次

2.递归方程:max=Math.max(max,f(s.substring(left,right),k))

例题5: 904. 水果成篮

class Solution {    
    public int totalFruit(int[] tree) {        
        if(tree.length==0 || tree.length==1 ) return tree.length;        
        //读题:获得只属于两种类型的最长子数组        
        int max=0,left=0,right=1;        
        Map<Integer,Integer> map=new HashMap<>();        
        map.put(tree[left],left);        
        int temp1=tree[left],temp2=tree[right];                
        while(right<tree.length){            
            int value=tree[right];            
            if(tree[right-1]!=value) map.put(value,right);            
            if(map.size()>2){                
                int pos1=map.get(temp1);                
                int pos2=map.get(temp2);                
                if(pos1<pos2){                    
                    map.remove(temp1);                    
                    temp1=temp2;                
                }else{                    
                    map.remove(temp2);                
                }                
                max=Math.max(max,right-left);                
                left=map.get(temp1);            
            }            
            if(value!=temp1) temp2=value;            
            right++;        
        }        
        max=Math.max(max,right-left);        
        return max;    
    }
}
执行用时:29 ms, 在所有 Java 提交中击败了56.98%的用户
内存消耗:47.8 MB, 在所有 Java 提交中击败了80.20%的用户

思路:

  1. 题意:就是找出只含两个元素的最长子数组
  2. 滑动窗口法:用hashMap做容器,右边移动停止的条件是容器有三个元素的时候,此时左边界移动到上一个元素的最左位置
  3. 实现方式:
  • 用hashmap做容器计算此刻子数组的元素个数,超过三个右边界停止移动,计算最大长度。
  • 左边界应该移动到上一元素连续出现的最左边位置,比如[1,2,1,1,3]当右边界遇到3的时候,左边界移动到第三个1的位置。所以在存储位置的时候,遇到连续元素,只存储最左边的位置。

总结:

  1. 滑动窗口法的应用场合:解的形式是【连续的】字符串、数组;

  2. 滑动窗口法的时间复杂度:一般为O(n)

  3. 滑动窗口法的代码结构:

    //初始值一般为0,0 或者0,1 int left=0; int right=1;

    while(right<s.length()){ while(right<s.length()){ if(右边界循环推出条件){执行的操作,俩指标怎么移动} right怎么移动; } (可能有退出条件) while(left<right,也可能是<=){ ...; left和right的移动; } (可能有退出条件) }