滑动窗口怎么理解?就我目前现在的理解来所就是将一个固定大小方框套在数组的身上呢个,然后不断移动这个方框从而获取不同范围的数值
滑动窗口的力所不及
在套模板的同时,大家是否考虑过,假设题目同样是求连续的子数组,但是在数组中出现了负数,那这种情况下还可以使用滑动窗口么?
答案是不行的,为什么?
我们窗口滑动的条件是什么,while窗口内元素超过或者不满足条件时移动,但如果数组存在负数,遇到不满足题意的时候,我们应该移动窗口左边界,还是扩大窗口右边界从而寻找到符合条件的情况呢?当一种场景存在多种可能时,显然就是当前的算法不适配解题。通常使用另一种数组中常用的算法----前缀和。
滑动窗口在数组中的应用
例题1: 给你一个 下标从 0 开始 的整数数组 nums ,其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k 。从数组中选出任意 k 名学生的分数,使这 k 个分数间 最高分 和 最低分 的 差值 达到 最小化 。
返回可能的 最小差值 。(力扣:1984)
// 看了看大神的思路确实很吊,排序加上滑动窗口
// 滑动窗口:通过两个指针截取固定长度的数组
class Solution {
public int minimumDifference(int[] nums, int k) {
// 先将数组进行排序
Arrays.sort(nums);
// 截取一定长度的数组
int left=0;
int right=k-1;
// 创建一个变量保存最小的差值
int min=Integer.MAX_VALUE;
while(right<nums.length){
if(nums[right]-nums[left]<min){
min=nums[right]-nums[left];
}
left++;
right=left+k-1;
}
return min;
}
}
例题2: 给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。任何误差小于 10-5 的答案都将被视为正确答案。(力扣:643)
这道题最难解决的地方就是重复求滑动窗口的和会超时
// 长度为k说明滑动窗口的大小已经固定了,我们只需要改变左指针从而改变滑动窗口的位置即可
class Solution {
public double findMaxAverage(int[] nums, int k) {
int n=nums.length,left=0,right=0; //使用双指针降低空间复杂度
int windowSum=0,res=Integer.MIN_VALUE;
while(right<n){
windowSum+=nums[right]; //一直扩大窗口
if(right-left+1==k){ //窗口的约束
res=Math.max(windowSum,res); //更新最大值
windowSum-=nums[left];
left++;
}
right++;
}
return (double)res/k;
}
}
// 官解
class Solution {
public double findMaxAverage(int[] nums, int k) {
//计算连续k个子数组中和最大的
int sum=0,max,n=nums.length;
for(int i=0;i<k;i++) sum+=nums[i];
max=sum;
for(int i=k;i<n;i++){
sum=sum-nums[i-k]+nums[i];
max=Math.max(max,sum);
}
return (double)max/k;
}
}
例题3: 给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。力扣(offer 008)
第一种做法:我想的是先固定左,然后移动右边界改变窗口的大小,然后移动左边界改变窗口的起点位置,依次类推,可惜超时了
// 连续子数组,我看见这句话就兴奋了,摆明了叫我我使用滑动窗口
// 相当与窗口大小会变化的滑动窗口问题
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 创建一个变量来记录最短的子数组
int minlen=Integer.MAX_VALUE;
// 创建两个指针来控制滑动窗口的位置
int left=0;
int right;
// 通过一个外层的位置来控制滑动窗口的位置
while(left<nums.length){
int sum=0;
// 通过控制right的位置来控制滑动窗口的大小
for(right=left;right<nums.length;right++){
sum=sum+nums[right];
if(sum>=target){
int temp=right-left+1;
minlen=minlen<temp?minlen:temp;
}
}
// 改变窗口的位置
left++;
}
return minlen==Integer.MAX_VALUE?0:minlen;
}
}
第二中方法:先不断移动右边界,找出可行解,然后移动左边界找出最优解
// 知道是使用滑动窗口可是自己的滑动窗口超时了
// 别人的思路还是吊,先不断移动右边界,找出可行解,然后移动左边界找出最优解
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 创建两个边界来记录窗口的位置
int lpoint=0;
int rpoint=0;
int minlen=nums.length+1;
// 创建一个变量来记录滑动窗口内的数字和
int sum=0;
// 通过for循环先以东右指针,找到可行解
for(;rpoint<nums.length;rpoint++){
sum=sum+nums[rpoint];
// 当出现了可行解后,缩小做边界寻找最优解
// 注意左边界是可以和右边界重合的,所以是<=
while(lpoint<=rpoint&&sum>=target){
int temp=rpoint-lpoint+1;
minlen=minlen<temp?minlen:temp;
sum=sum-nums[lpoint++];
}
}
return minlen==nums.length+1?0:minlen;
}
}
例题4: 给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。(Offer 009)
// 很喜欢滑动窗口是吧?今天我就感死你
// 我感这道日的思路根之前的思路是不一样,之前的是求最小连续的子数组所以
// 可以是在可行解的条件下寻找最优解,可这道题不一样,它是要求找到所有可行的数组
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
int right=0,left=0;
int ans=0;
long pro=1;
for(;right<nums.length;right++){
pro*=nums[right];
// 如果*上当前位置的值都已经大于target
// 所以后面的连续组合必然是不成立的,所以要将左边界进型移动
while(left<=right&&pro>=k){
pro/=nums[left++];
}
// 统计以当前节点为有边界的符合条件的数组的子数组
// 因为以当前的节点为边界的数组的符合条件的子数组的个数就等于right-left+1;
ans+=right-left+1;
}
return ans;
}
};
例题5: 生日蜡烛
某君从某年开始每年都举办一次生日party,并且每次都要吹熄与年龄相同根数的蜡烛。
现在算起来,他一共吹熄了236根蜡烛。
请问,他从多少岁开始过生日party的?
package two;
public class Two_4 {
// 我认为是滑动窗口
// 好题:弥补我了滑动窗口题的细节,就是我们先进行sum+,还是先进行判断
// 其实两种方式都没问题,关键需要理解左指针的位置,如果先进行判断在加的话,那么我们进入循环的
// 优化循环的条件应该就是left<right-1,因为进行优化时,right所指向的值还没有添加到sum中
// 如果时先加再进行判断则优化循环的条件就是left<right
public static void main(String[] args) {
int[] nums=new int[100];
for(int i=0;i<100;i++) {
nums[i]=i;
}
// 创键两个指针控制滑动窗口的位置
int left=1;
int right;
// 创建一个变量来保存蜡烛的数量
int sum=0;
//进行滑动窗口的移动,在可行解的前提下寻找最优解
for(right=1;right<nums.length;right++) {
// 不断将右指针所指向的数值相加
// sum=sum+nums[right];
// 当可行解出现后寻找最优解
while(sum>236&&left<right-1) {
sum=sum-nums[left];
left++;
// 如果sum刚好等于236直接返回左指针即可
if(sum==236) {
System.out.println(left);
return;
}
}
// 我去,一定要注意下面着行代码的位置
sum=sum+nums[right];
}
}
}
滑动窗口在字符串中的应用
例题1: 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。(力扣:3)
// 这道题的滑动窗口的思路:类似于双从循环,怎么说?就是先固定左边界然后移动有边界
// 原来我之前理解错了:下面的根本就不是滑动窗口,只是简单的暴力双重循环,我就觉得奇怪
class Solution {
public int lengthOfLongestSubstring(String s) {
// 创建两个变量来控制滑动窗口的边界
int left=0;
int right;
// 创建一个变量来记录最长的字串长度
int maxlen=0;
for(;left<s.length();left++){
// 创建一个集合判断滑动窗口内是否有重复的字符
List<Character> list=new ArrayList<Character>();
// 不断移动有指针,并将其加入集合
right=left;
while(right<s.length()){
if(!list.contains(s.charAt(right))){
list.add(s.charAt(right));
}else{
break;
}
right++;
}
// 比较这次循环的最长子串
if(right-left>maxlen){
maxlen=right-left;
}
}
return maxlen;
}
}
方法2:滑动窗口
// 滑动窗口的思路:不断扩大有边界,直到出现限制,开始移动左边界,直到窗口符合限制,以此类推
class Solution {
public int lengthOfLongestSubstring(String s) {
// 创建两个变量作为滑动窗口的边界
int lpoint=0;
int rpoint=0;
// 创建一个集合判断窗口内的字符串是否出现了重复
List<Character> list=new ArrayList<>();
// 创建一个变量来保存最长的字符串
int maxlen=0;
for(;rpoint<s.length();rpoint++){
// 如果出现了重复情况就开始缩小左边界
while(lpoint<rpoint&&list.contains(s.charAt(rpoint))){
// 将左边界不断向右移动,并将最前面的字符从集合种去掉
list.remove(0);
lpoint++;
}
// 不断将右边界加入集合,直到出现重复的情况
list.add(s.charAt(rpoint));
// 进行最长字串判断
maxlen=rpoint-lpoint+1>maxlen?rpoint-lpoint+1:maxlen;
}
return maxlen;
}
}
例题2: 给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。换句话说,第一个字符串的排列之一是第二个字符串的 子串 。(Offer 014)
// 大神的思路还是吊:使用滑动窗口,同时维护两个数组,判断滑动窗口里面的字符是否相等
class Solution {
public boolean checkInclusion(String s1, String s2) {
// 首先创建两个数组
int[] arr1=new int[26];
int[] arr2=new int[26];
// 避免异常情况的产生
if(s1.length()>s2.length()){
return false;
}
// 先将两个数组初始化
for(int i=0;i<s1.length();i++){
arr1[s1.charAt(i)-'a']++;
arr2[s2.charAt(i)-'a']++;
}
// 先进行一次判断万一成了呢?
// 这里涉及到了一个新的api:Arrays.equals(int[],int[]);
// 可以判断两个数组是否相等
if(Arrays.equals(arr1,arr2)){
return true;
}
// 通过for循环对滑动窗口进行移动
for(int i=s1.length();i<s2.length();i++){
// 首先对新加入的字符进行统计
arr2[s2.charAt(i)-'a']++;
// 因为滑动窗口的大小是固定的所以要将最左边的字符给除掉
arr2[s2.charAt(i-s1.length())-'a']--;
// 每一次循环都判断两个数组是否相等
if(Arrays.equals(arr1,arr2)){
return true;
}
}
return false;
}
}
例题3: 给定两个字符串 s 和 p,找到 s 中所有 p 的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。变位词 指字母相同,但排列不同的字符串。
第一种:双重循环
// 这道题的滑动窗口的思路:类似于双从循环,怎么说?就是先固定左边界然后移动有边界
// 原来我之前理解错了:下面的根本就不是滑动窗口,只是简单的暴力双重循环,我就觉得奇怪
class Solution {
public int lengthOfLongestSubstring(String s) {
// 创建两个变量来控制滑动窗口的边界
int left=0;
int right;
// 创建一个变量来记录最长的字串长度
int maxlen=0;
for(;left<s.length();left++){
// 创建一个集合判断滑动窗口内是否有重复的字符
List<Character> list=new ArrayList<Character>();
// 不断移动有指针,并将其加入集合
right=left;
while(right<s.length()){
if(!list.contains(s.charAt(right))){
list.add(s.charAt(right));
}else{
break;
}
right++;
}
// 比较这次循环的最长子串
if(right-left>maxlen){
maxlen=right-left;
}
}
return maxlen;
}
}
第2种方法:滑动窗口
// 这道题根上面的哪一道题应该是一样的思路:都是使用数组数组统计滑动窗口内的字符的个数
// 然后比较两个数组是否相等,无非是多了一个将左边解加入集合的操作
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 创建一个集合保存起始索引
List<Integer> list=new ArrayList<Integer>();
// 创建两个数组统计滑动窗口内的字符的个数
int[] sarr=new int[26];
int[] parr=new int[26];
int slen=s.length();
int plen=p.length();
// 避免异常情况的产生
if(plen>slen){
return list;
}
// 初始化上面的连个数组
for(int i=0;i<plen;i++){
sarr[s.charAt(i)-'a']++;
parr[p.charAt(i)-'a']++;
}
// 先对初始情况进行一次判断
if(Arrays.equals(sarr,parr)){
list.add(0);
}
// 通过for循环移动滑动窗口,并判断窗口内的字符串是否相等
for(int i=plen;i<slen;i++){
sarr[s.charAt(i)-'a']++;
sarr[s.charAt(i-plen)-'a']--;
if(Arrays.equals(sarr,parr)){
// 将符合的窗口的左边界加入集合
list.add(i-plen+1);
}
}
return list;
}
}
例题3: 给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 “” 。
如果 s 中存在多个符合条件的子字符串,返回任意一个。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
来源:力扣(Offer 017)
// 这道题似乎已经做过了:思路:使用一个数组保存窗口内的字符的个数,使用数组保存待找
// 字符串的字符个数,如果两个数组的内容是一致的就将此时滑动窗口的长度记录下来
// 做着做着发现了两道题的不同,那道题是寻找连续的子字符串,可这道题它不要求是连续的只要包含即可
// 解决方法:只能自己写一个判断sarr中是否包含tarr中的所有字母作为窗口缩小的条件
// 我去使用数组好麻烦啊,它s,t的大小写不一定一直,s中也不宜定全是小写
// 还是使用mao代替数组比较好
// 成功解决这道困难题
class Solution {
public String minWindow(String s, String t) {
// 创建两个map分别保存连个字符串的字符的个数
Map<Character,Integer> smap=new HashMap<>();
Map<Character,Integer> tmap=new HashMap<>();
if(s.length()<t.length()){
return "";
}
if(s.equals(t)){
return s;
}
// 创建一个变量来记录最短的字符串
int minlen=Integer.MAX_VALUE;
String str="";
// 首先初始化t字符串数组
for(int i=0;i<t.length();i++){
tmap.put(t.charAt(i),tmap.getOrDefault(t.charAt(i),0)+1);
}
// 创建两个变量来控制滑动窗口的位置
int lpoint=0;
int rpoint=0;
// 不断移动右指针,扩大窗口的边界
for(;rpoint<=s.length();rpoint++){
// 当sarr中包含了tarr中的所有字符是开始缩小左边界,寻找最短字符串
// 感觉一进来就得先进行以此判断
while(lpoint<rpoint&&pd(smap,tmap)){
// 记录短的字符串长度
if(rpoint-lpoint<minlen){
minlen=rpoint-lpoint;
str=s.substring(lpoint,rpoint);
}
// 左边界右移,同时将该字符从sarr中移除
smap.put(s.charAt(lpoint),smap.getOrDefault(s.charAt(lpoint),0)-1);
lpoint++;
}
if(rpoint<s.length()){
smap.put(s.charAt(rpoint),smap.getOrDefault(s.charAt(rpoint),0)+1);
}
}
return str;
}
// 创建一个函数判断sarr中是否包含了tarr中的所有字母
public boolean pd(Map smap,Map tarr){
// 获取tarr中的keySet
Set tkey=tarr.keySet();
Iterator it=tkey.iterator();
while(it.hasNext()){
Object key=it.next();
// 将Object类型转化未整数
int temp1=Integer.parseInt(smap.getOrDefault(key,0).toString());
int temp2=Integer.parseInt(tarr.getOrDefault(key,0).toString());
if(temp1<temp2){
return false;
}
}
return true;
}
}