滑动窗口算法

本文介绍了如何使用滑动窗口策略解决一系列IT技术问题,如长度最小的子数组、无重复字符的最长子串、最大连续1的个数、将x减到0的操作数、水果成篮、字母异位词匹配、串联所有单词的串以及最小覆盖子串等,展示了滑动窗口算法在处理连续区间问题上的高效性。
摘要由CSDN通过智能技术生成


滑动窗口过程:
进窗口->判断->更新结果->出窗口

长度最小的子数组

算法思路:
由于分析的对象是「⼀段连续的区间」,因此可以考虑用「滑动窗口」来解决。

窗口左端为nums[left],右端下一个元素为nums[right]. 将nums[right]划入窗口中(sum += nums[right] )

  • 如果窗口内元素之和大于等于 target :记录出此时窗口内元素数量,并且将左端元素划出继续判断是否满足条件,如果满足更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
  • 如果窗口内元素之和不满足条件: right++ ,放下⼀个元素进入窗口。

为何滑动窗口可以解决问题,并且时间复杂度更低?
因为我们在求第⼀段区间的时候,已经算出很多元素的和了,这些和是可以在计算
下次区间和的时候用上, 将 nums[left]剔除, 就能判断下一个区间, 这样就能省掉大量重复的计算。

时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者最多都往后移动 n 次。因此时间复杂度是 O(N) 。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length, sum = 0, len = Integer.MAX_VALUE;
        for(int left = 0, right = 0;right < n; right++) {
            sum += nums[right];
            while(sum >= target) {
                len = Math.min(len, right-left+1);
                sum -= nums[left++];
            }
        } 
        return len == Integer.MAX_VALUE ? 0:len;
    }
}

无重复字符的最长子串

算法思路:
研究的对象依旧是⼀段连续的区间,因此可以使用「滑动窗口」思想来优化。

让滑动窗口满足:窗口内所有元素都是不重复的。
右端元素进入窗口的时候,哈希表统计这个字符在窗口内的频次:
▪ 如果这个字符出现的频次超过 1 ,说明窗口内有重复元素,那么就从左侧划出元素,直到重复的元素的频次变为 1 ,然后再更新结果(ret)。
▪ 如果没有超过1,说明当前窗口没有重复元素,可以直接更新结果(ret)

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char[] str = s.toCharArray();
        int[] hash = new int[128];//数组模拟哈希表
        int left = 0, right = 0, n = str.length;
        int ret = 0;
        while(right < n) {
            hash[str[right]]++;
            while(hash[str[right]] > 1) {
                hash[str[left]]--;
                left++;
            }
            ret = Math.max(ret, right-left+1);
            right++;
        }
        return ret;
    }
}

最大连续1的个数III

算法思路(滑动窗口):
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是⼀段连续的 1 中间塞了 k个 0 嘛。
因此,我们可以把问题转化成:求数组中⼀段最长的连续区间,要求这段区间内 0 的个数不超过 k 个。既然是连续区间,可以考虑使用「滑动窗口」来解决问题。

算法流程:

  1. 用变量zero统计窗口内0的个数;初始化⼀些变量 left = 0 ,right = 0 , ret = 0 ;
  2. 当 right 小于数组长度的时候,⼀直下列循环:

i. 让右侧元素进入窗口,如果是0, zero++
ii. 检查 0 的个数是否超过k, 如果超过,依次让左侧元素滑出窗口,直到 0 的个数恢复正

iii. 程序到这里,说明窗口内元素是符合要求的,更新结果(ret);
iv. right++ ,处理下⼀个元素;

  1. 循环结束后, ret 存的就是最终结果。
class Solution {
    public int longestOnes(int[] nums, int k) {
        int ret = 0;
        for(int left = 0, right = 0, zero = 0; right < nums.length; right++) {
            if(nums[right] == 0) zero++;
            while(zero > k) {
                if(nums[left++] == 0) {
                    zero--;
                }
            }
            ret = Math.max(ret, right - left + 1);
        }
        return ret;
    }
}

还有一种:

每次循环right都会向后一位

  • 如果K >= 0,left不会向后一位
  • 如果K < 0,left也会向后一位

所以 right - left 不会变小

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

将x减到0的最小操作数

算法思路(滑动窗口):
题目要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最长数组。因为元素>=1。此时,就是熟悉的「滑动窗口」问题了。

算法流程:

  1. 转化问题: target = sum(nums) - x 。如果 target < 0 ,问题无解;
  2. 初始化左右指针 left = 0 , right = 0 ;初始化记录滑动窗口内元素之和的变量sum = 0
  3. 当 right 小于等于数组长度时,⼀直循环:

⑴将right对应的元素划入窗口,如果 tmp > target, 循环减去左侧元素直至tmp <= target
⑵判断tmp == target,

  • 如果相等,更新ret,进入下次循环
  • 如果不等,说明 tmp < target, 进入下次循环
  1. 循环结束后,如果 ret 的值有意义,则计算结果返回;否则,返回 -1 。
class Solution {
    public int minOperations(int[] nums, int x) {
        int sum = 0;
        for(int a: nums) sum += a;
        int target = sum - x;
        //处理细节
        if(target < 0) return -1;

        int ret = -1;
        for(int left = 0, right = 0, tmp = 0; right <nums.length; right++) {
            tmp += nums[right];
            while(tmp > target) {
                tmp -= nums[left++];
            }
            if(tmp == target) {
                ret = Math.max(ret, right - left + 1);
            }
        }
        return ret == -1 ? ret: nums.length - ret;
    }
}

水果成篮

算法思路(滑动窗口):
研究的对象是⼀段连续的区间,可以使用「滑动窗口」思想来解决问题。
让滑动窗口满足:窗口内水果的种类只有两种。
做法:右端水果进入窗口的时候,用哈希表统计这个水果的频次,判断哈希表的长度:

  • 如果长度大于 2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出,直到哈希表的大小小于等于 2,然后更新结果(ret);
  • 如果没有超过 2,说明当前窗⼝内水果的种类不超过两种,直接更新结果 ret。

算法流程:

  1. 用Map来统计窗口内水果的种类和数量;
  2. 初始化变量:左右指针 left = 0,right = 0,记录结果的变量 ret = 0;
  3. 当 right 小于数组长度的时候,⼀直执行下列循环:

i. 将右侧水果放入哈希表中;
ii. 判断右侧水果进来后,哈希表的长度:

如果超过 2:

  • 将左侧元素滑出窗⼝,并且在哈希表中将该元素的频次-1;
  • 如果这个元素的频次-1之后变成了 0,就把该元素从哈希表中删除;
  • 重复上述两个过程,直到哈希表的长度不超过 2;

iii. 更新结果 ret;
iv. right++,让下⼀个元素进⼊窗口;

  1. 循环结束后,ret 存的就是最终结果

一、 使用容器

class Solution {
    public int totalFruit(int[] fruits) {
        Map<Integer, Integer> hash = new HashMap<Integer, Integer>();
        int ret = 0;
        for(int left = 0, right = 0; right < fruits.length; right++) {
            hash.put(fruits[right], hash.getOrDefault(fruits[right], 0) + 1);
            while(hash.size() > 2) {
                hash.put(fruits[left], hash.get(fruits[left]) - 1);
                if(hash.get(fruits[left]) == 0) {
                    hash.remove(fruits[left]);
                }
                left++;
            }
            ret = Math.max(ret, right - left +1);
        } 
        return ret;
    }
}

二、 用数组模拟哈希表

kinds统计窗口内水果的种类

class Solution {
    public int totalFruit(int[] fruits) {
        int n = fruits.length;
        int[] hash = new int[n];//模拟哈希表
        
        int ret = 0;
        for(int left = 0, right = 0, kinds = 0; right < n; right++) {
            if(hash[fruits[right]] == 0) kinds++;
            hash[fruits[right]]++;
            while( kinds > 2) {
                hash[fruits[left]]--;
                if(hash[fruits[left]] == 0) kinds--;
                left++;
            }
            //更新结果
            ret = Math.max(ret, right - left +1);
        } 
        return ret;
    }
}

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

算法思路(滑动窗口 + 哈希表):

因为字符串 p 的异位词的长度⼀定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造⼀个长度与字符串 p 相同的滑动窗口,并在滑动中维护窗口中字母的数量。当窗口中每种字母的数量与字符串 p 相同时,说明当前窗口为字符串 p的异位词。
可以用两个大小为 26 的数组来模拟哈希表(全是小写字母),⼀个来保存 s 中的子串每个字符出现的个数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。

count统计符合要求的字符个数

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ret = new ArrayList<>();
        char[] pp = p.toCharArray();
        char[] ss = s.toCharArray();  

        int[] hash1 = new int[26];  
        for(char ch: pp)  hash1[ch-'a']++;

        int[] hash2 = new int[26];  
        int m = pp.length;
        for(int left = 0, right = 0, count = 0; right < ss.length; right++) {
            char in = ss[right];
            if(++hash2[in-'a'] <= hash1[in-'a']) count++;
            if(right - left + 1 > m)  {
                char out = ss[left++];
                if(hash2[out-'a'] <= hash1[out-'a']) {
                    count--;
                }
                hash2[out-'a']--;
            }
            //更新结果
            if(count == m) ret.add(left);
        }
        return ret;
    }
}

串联所有单词的串

如果我们把单词看成字母,问题就变成了找到「字符串中所有的字母异位词」。无非就是之前处理的对象是一个一个的字符,我们这里处理的对象是⼀个⼀个的单词。

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<>();
        Map<String,Integer> hash1 = new HashMap<>();
        for(String str : words) hash1.put(str, hash1.getOrDefault(str, 0) + 1);

        int len = words[0].length(), m= words.length;
        for(int i = 0; i < len; i++) {
            Map<String,Integer> hash2 = new HashMap<>();
            for(int left = i, right = i, count = 0; right + len <= s.length(); right += len) {
                String in = s.substring(right, right+len);
                hash2.put(in, hash2.getOrDefault(in, 0)+1);
                if(hash2.get(in) <= hash1.getOrDefault(in, 0)) count++;
                if(right - left + 1 > len * m) {
                    String out = s.substring(left, left +len);
                    if(hash2.get(out) <= hash1.getOrDefault(out, 0)) count--;
                    hash2.put(out, hash2.get(out) - 1);
                    left += len;
                }
                if(count == m) ret.add(left);
            }
        }
        return ret;
    }
}

最小覆盖子串

算法思路(滑动窗口+ 哈希表):
研究对象是连续的区间,因此可以使用滑动窗口的思想来解决。

我们可以使用两个哈希表,其中⼀个将目标串的信息统计起来,另⼀个哈希表动态维护窗口内字符串的信息。

使用count标记有效字符的种类

class Solution {
    public String minWindow(String s, String t) {
        char[] ss = s.toCharArray();
        char[] tt = t.toCharArray();

        int[] hash1 = new int[128];//统计t中元素的频次
        int kinds = 0;//t中有多少种字符
        for(char ch: tt) {
            if(hash1[ch]++ == 0) {
                kinds++;
            }
        }
        int[] hash2 = new int[128];//统计窗口中字符的频次

        int minlen = Integer.MAX_VALUE, begin = -1;
        for(int left = 0, right = 0, count = 0; right < ss.length; right++) {
            char in = ss[right];
            if(++hash2[in] == hash1[in]) count++;
            while(kinds == count) {
                if(right - left + 1 < minlen) {
                    begin = left;
                    minlen = right - left + 1;
                }
                char out = ss[left++];
                if(hash2[out]-- == hash1[out]) count--;
            }
        }
        if(begin == -1) return new String();
        else return s.substring(begin, begin + minlen);
    }
} 

小葱的01串

注意是给定一个长度为偶数的环形 01 字符串

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        char[] s = in.next().toCharArray();
        
        int[] sum = new int[2];
        for(int i = 0; i < n; i++) sum[s[i] - '0']++;
        
        int left = 0, right = 0, ret = 0, half = n/2;
        int[] count = new int[2];
        
        while(right < n-1) { //细节问题
            count[s[right] - '0']++;
            if(right - left + 1 > half) count[s[left++] - '0']--;
            if(right - left + 1 == half) {
                if(count[0] * 2 == sum[0] && count[1] * 2 == sum[1]) ret += 2;
            }
            right++;
        }
        System.out.print(ret);
    }
}

空调遥控

只需要找到范围小于等于2p的最大窗口即可

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt(), p = in.nextInt();
        int[] arr = new int[n];
        for(int i = 0; i < n; i++) {
            arr[i] = in.nextInt();            
        }
        Arrays.sort(arr);
        
        int left = 0, right = 0, ret = 0;
        p *= 2;
        while(right < n) {
            while(arr[right] - arr[left] > p) left++;
            ret = Math.max(ret, right - left + 1);
            right++;
        }
        System.out.print(ret);
    }
}

小红的子串

import java.util.Scanner;

public class Main {
    static int n, l, r;
    static char[] s;
    
    static long find(int x) {
        if(x == 0) return 0;
        //滑动窗口
        int left = 0, right = 0;
        int[] hash = new int[26];
        int kinds = 0;//统计窗口内字符的种类
        long ret = 0;
        
        while (right < n) {
            if (hash[s[right]-'a']++ == 0) kinds++;
            while (kinds > x) {
                if(hash[s[left]-'a']-- == 1) kinds--;
                left++;
            }
            ret += right - left + 1;
            right++;
        }
        return ret;
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt(); l = sc.nextInt(); r = sc.nextInt();
        s = sc.next().toCharArray();

        System.out.println(find(r)-find(l-1));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值