当前位置: 首页 > news >正文

Java优选算法——滑动窗口

目录

一、长度最小的子数组

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

三、最大连续1的个数 Ⅲ

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

五、水果成篮

⭐️六、找到字符串中所有字母异位词

⭐️七、串联所有单词的子串

⭐️八、最小覆盖子串


滑动窗口解法的适用条件是:

  • 寻找一段连续的区间
  • 左右指针向右滑动对最终结果有一定的规律影响

⭐️探究的是以不同的左指针开始,到右指针的这段区间内的性质。右指针到底了,这个算法就结束了。代码逻辑中着重考虑的是:

出窗口:

左指针什么时候更新,如何更新。

进窗口:

更新之后怎么做

进窗口{出窗口{}}

一、长度最小的子数组

题目链接:209. 长度最小的子数组 - 力扣(LeetCode)

题目:

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的子数组 [numsₗ, numsₗ₊₁, ..., numsᵣ₋₁, numsᵣ] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

 

示例 1:
输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

 

示例 2:
输入: target = 4, nums = [1,4,4]
输出: 1

 

示例 3:
输入: target = 11, nums = [1,1,1,1,1,1,1,1]
输出: 0


思路:

由于此问题分析的对象是「一段连续的区间」,因此可以考虑「滑动窗口」的思想来解决这道题。
让滑动窗口满足:从 i 位置开始,窗口内所有元素的和小于 target(那么当窗口内元素之和第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度)。
做法:⭐️将右端元素划入窗口中,统计出此时窗口内元素的和:

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

代码及结果:

class Solution {public int minSubArrayLen(int target, int[] nums) {int n=nums.length,sum=0,len=Integer.MAX_VALUE;//让len等于无穷大而不是零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];left++;}}return len==Integer.MAX_VALUE?0:len;}
}


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

题目链接:3. 无重复字符的最长子串 - 力扣(LeetCode)

题目:

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

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

示例 2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是子串的长度,"pwke" 是一个子序列,不是子串。


思路:

这道题涉及到连续的区间、左右指针滑动后,区间内可能会出现重复的字符,所以使用滑动窗口解法。

可以使用set集合,将区间内的字符加入到集合中,后续移动右指针的时候判断set中是否存在这个字符。在算法题中,也尽可能使用数组模拟的哈希表来代替set,减少容器的创建。

数组模拟的哈希表,下标为键,对应元素为值,当前下标的元素值为0时,代表没有这个下标(没有这个键);但到了哈希表中,没有值的时候记得remove这个键。

  1. 在HashMap中右指针对应元素的value>1的时候更新左指针
  2. HashMap中左指针元素的value值-1,左指针右移一位
  3. 直到HashMap中右指针对应元素value不大于1,就停止更新
  4. 继续移动右指针,将HashMap中对应元素value值+1

代码及结果:

class Solution {public int lengthOfLongestSubstring(String s) {int [] hash=new int[128];//用数组模拟哈希表,下标为key,元素为valueint left=0,right=0,len=0;while(right<s.length()){hash[s.charAt(right)]++;//进入哈希表while(hash[s.charAt(right)]>1){hash[s.charAt(left)]--;left++;}len=Math.max(len,right-left+1);right++;}return len;}}


三、最大连续1的个数 Ⅲ

题目链接:1004. 最大连续1的个数 III - 力扣(LeetCode)

题目:

给定一个二进制数组 nums 和一个整数 k,假设最多可以翻转 k 个 0 ,则返回执行操作后数组中连续 1 的最大个数。  

示例 1:  
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2  
输出:6  
解释:[1,1,1,0,0,1,1,1,1,1,1]  
粗体数字从 0 翻转到 1,最长的子数组长度为 6。  

示例 2:  
输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3  
输出:10  
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]  
粗体数字从 0 翻转到 1,最长的子数组长度为 10。


思路:

题目涉及到一段连续的区间、左右指针滑动会导致“1”个数的变化,因此可以使用滑动窗口解法。

  • 如果窗口内,每进入一次“0”,我们可以将它看作“1”,并记做zero(后续+1)。
  • 如果zero>K就要更新左指针。
  • 先判断左指针对应的元素,如果是0,则zero-1;如果是1,zero不变。然后左指针右移。
  • 当zero<=K时,左指针停止更新。
  • 之后右指针右移,再进行判断。

代码及结果:

class Solution {public int longestOnes(int[] nums, int k) {int left=0,right=0,len=0,zero=0;for(right=0;right<nums.length;right++){if(nums[right]==0) zero++;while(zero>k){if(nums[left++]==0){zero--;}}len=Math.max(len,right-left+1);}return len;}
}


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

题目链接:1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

题目:

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要修改数组以供接下来的操作使用。  

如果可以将 x 恰好减到 0 ,返回最小操作数;否则,返回 -1 。

示例 1:  
输入:nums = [1,1,4,2,3], x = 5  
输出:2  
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。  

示例 2:  
输入:nums = [5,6,7,8,9], x = 4  
输出:-1  

示例 3:  
输入:nums = [3,2,20,1,1,3], x = 10  
输出:5  
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。


思路:

原题目是让我们从左边或者右边各找一些元素使其之和为x,但是左右并不是一个连续的区间,剩下的中间部分才是连续的区间。

假设数组中元素之和为sum,我们将问题转化为从数组中找一段连续的区间,它是元素之和为sum-x的最长区间即可,便可以用滑动窗口来解决这道题。

  • 当窗口内元素之和count大于sum-x,开始更新左指针。
  • 先将count减去左指针对应元素,左指针再右移。
  • 直到count小于或者等于sum-x,就停止更新。
  • 后续再移动右指针。

代码及结果:

class Solution {public int minOperations(int[] nums, int x) {int count=0;//当前窗口元素之和int sum=0;//数组所有元素之和for(int cur:nums){sum+=cur;}if(sum<x){return -1;}if(sum==x){return nums.length;}int len=0;//窗口长度的最大值int left=0;int right=0;while(right<nums.length){count+=nums[right];// 当窗口和超过目标值,且left不越界时,移动left缩小窗口while(count>sum-x&&left<=right){count-=nums[left++];}if(count==sum-x){len=Math.max(len,right-left+1);}right++;}return len==0?-1:nums.length-len;}
}


五、水果成篮

题目链接:904. 水果成篮 - 力扣(LeetCode)

题目:

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果种类。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有两个篮子,并且每个篮子只能装单一类型的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从每棵树(包括开始采摘的树)上恰好摘一个水果。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits,返回你可以收集的水果的最大数目。

示例 1:
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。


思路:

这道题依旧使用滑动窗口解答,我们可以创建一个哈希表。

随着右指针的右移,窗口内每进入一个数就放入哈希表中,对应的value值+1,当map的size大于2,就更新左指针。

将当前左指针的value值-1,然后左指针右移。

当map的size小于等于2,结束更新。

接下来右指针再继续向右移动。


代码及结果:

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


⭐️六、找到字符串中所有字母异位词

题目链接:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

题目:

给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba",它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac",它是 "abc" 的异位词。

示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab",它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba",它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab",它是 "ab" 的异位词。


思路:

将这道题题目转化为:在s中找到一些固定大小的窗口,窗口内的字符是p的异位词,返回窗口的left值。

❓怎么判断是否符合条件呢,创建两个hash表,一个存放p字符的元素情况,一个存放窗口内的元素情况。

⭐️方案一:

当这两个hash表中存放元素情况相等,就说明符合条件,将left加入结果数组中。

Arrays.equals(hash1,hash2)//判断两个数组是否相同

⭐️方案二:

引入一个变量count,让count记录hash2中有效字符的个数。

什么才算是有效字符呢,我们知道hash1存放的是字符串p的数据,如果hash2中某个字母出现的次数大于了hash1中出现的次数,那么这个hash2中就有多余的部分,多余的部分就不算有效字符。

比如:

  • hash1中a出现的次数是2,当窗口进入一个a后,hash2中a的次数是1,这个a就是有效的,count就要+1;
  • hash1中a出现的次数是2,当窗口进入一个a后,hash2中a的次数是3,这个a是多余的,count不变;
  • hash1中a出现的次数是2,hash2中a的次数是3,当这个a出窗口时,这个a是多余的,count不变;

只要hash2[字符-'a']<=hash1[字符-'a'],就是有效元素。

⭐️代码逻辑:

每次进窗口和出窗口时,我们都要判断一下当前要操作的元素,如果

left和right的初始值为0;

当right-left+1>plen(当right和left之间的距离超过固定大小了,就要移动left),就更新left;

先从hash表中减少left对应元素,left++;


代码及结果:

方案一:

class Solution {public List<Integer> findAnagrams(String ss, String pp){List<Integer> ret = new ArrayList<Integer>();char[] s = ss.toCharArray();char[] p = pp.toCharArray();int[] hash1 = new int[26]; // 统计字符串 p 中每一个字符出现的个数for(char ch : p) hash1[ch - 'a']++;int[] hash2 = new int[26]; // 统计窗口中每一个字符出现的个数for(int left=0,right=0;right<s.length;right++){char in=s[right];hash2[in-'a']++;//进窗口if(right-left+1>p.length){char out=s[left];hash2[out-'a']--;left++;}if(Arrays.equals(hash1,hash2)){ret.add(left);}}return ret;}
}

方案二:

class Solution {public List<Integer> findAnagrams(String ss, String pp){List<Integer> ret = new ArrayList<Integer>();char[] s = ss.toCharArray();char[] p = pp.toCharArray();int[] hash1 = new int[26]; // 统计字符串 p 中每一个字符出现的个数for(char ch : p) hash1[ch - 'a']++;int[] hash2 = new int[26]; // 统计窗口中每一个字符出现的个数int m = p.length;for(int left = 0, right = 0, count = 0; right < s.length; right++){char in = s[right];if(++hash2[in - 'a'] <= hash1[in - 'a']) count++; // 进窗口 + 维护 countif(right - left + 1 > m) // 判断{char out = s[left++];if(hash2[out - 'a']-- <= hash1[out - 'a']) count--; // 出窗口 + 维护 count}// 更新结果if(count == m) ret.add(left);}return ret;}
}


⭐️七、串联所有单词的子串

题目链接:30. 串联所有单词的子串 - 力扣(LeetCode)

题目:

给定一个字符串 s 和一个字符串数组 words 。words 中所有字符串长度相同。

s 中的串联子串是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

 
  • 例如,如果 words = ["ab","cd","ef"],那么 "abcdef""abefcd""cdabef""cdefab""efabcd",和 "efcdab" 都是串联子串。"acdbef" 不是串联子串,因为他不是任何 words 排列的连接。
 

返回所有串联子串在 s 中的开始索引。你可以以任意顺序返回答案。

 

示例 1:
输入: s = "barfoothefoobarman", words = ["foo","bar"]
输出: [0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。

 

示例 2:
输入: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出: []
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。

 

示例 3:
输入: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出: [6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。


思路:

这道题与上一道题的区别不大,words中的每个元素都是一个字符串,我们把它看作一个整体(就像看作一个字母一样),在s中我们划分多个小段,每段的长度都是words[0].length(),每一段看作一个整体。

就相当于在s中找一段区间,这段区间是由words的异位词组成的一样。


代码及结果:

解法一:

class Solution {public List<Integer> findSubstring(String s, String[] words) {List<Integer> ret = new ArrayList<>();if (words == null || words.length == 0 || s == null) {return ret;}int len = words[0].length();int totalLen = words.length * len;if (s.length() < totalLen) {return ret;}Map<String, Integer> hash1 = new HashMap<>(); // 存放words中的元素for (String word : words) {hash1.put(word, hash1.getOrDefault(word, 0) + 1);}// 遍历所有可能的起始偏移量(0到len-1)for (int i = 0; i < len; i++) {Map<String, Integer> hash2 = new HashMap<>(); // 存放窗口内的元素int left = i;for (int right = i; right <= s.length() - len; right += len) {// 截取当前单词(修正拼写错误和索引计算)String in = s.substring(right, right + len);hash2.put(in, hash2.getOrDefault(in, 0) + 1);// 当窗口大小超过目标长度时,移动左指针//注意窗口长度是right-left+len//因为left和right指向的是s的每一段开头位置while (right - left + len > totalLen) {String out = s.substring(left, left + len);hash2.put(out, hash2.get(out) - 1);if (hash2.get(out) == 0) {hash2.remove(out);}left += len;}// 检查当前窗口是否匹配if (hash1.equals(hash2)) {ret.add(left);}}}return ret;}
}

解法二:

class Solution {public List<Integer> findSubstring(String s, String[] words) {List<Integer> ret=new ArrayList<>();int left,right;int len=words[0].length();Map<String,Integer> hash1=new HashMap<>();for(int i=0;i< words.length;i++){hash1.put(words[i],hash1.getOrDefault(words[i],0)+1);}for(int i=0;i<len;i++){int count=0;//定义窗口中与words中相同单词的数量,if(count==words.length),说明窗口内符合条件Map<String,Integer> hash2=new HashMap<>();for(left=i,right=i;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++;//判断是否出窗口//注意窗口长度是right-left+len//因为left和right指向的是s的每一段开头位置if(right-left+len>len* words.length){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== words.length) ret.add(left);}}return ret;}
}


⭐️八、最小覆盖子串

题目链接:76. 最小覆盖子串 - 力扣(LeetCode)

题目:

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

 

注意:

 
  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。
 

示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B' 和 'C'

 

示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

 

示例 3:
输入:s = "a", t = "aa"
输出:""
解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。


思路:

同样的,这道题要使用到哈希表来判断当前窗口是否符合条件。

如果使用hash1存储t,hash2存放窗口内的元素,判断hash1.equals(hash2)是不行的。

因为:

hash1 存储的是 t 中每个字符的「最低需求数量」(例如 t = "AABC" 时,hash1 为 {'A':2, 'B':1, 'C':1})。
而滑动窗口的有效条件是:窗口内的字符(hash2)需包含 hash1 中所有键,且对应的值都不小于 hash1 的值(例如 hash2 为 {'A':3, 'B':1, 'C':2} 是有效的)。

⭐️引入count来表示有效元素的个数。

代码逻辑:

当窗口符合条件时,先减少窗口hash表中left元素的数量,再更新左指针;


代码及结果:

class Solution {
public String minWindow(String s, String t) {String ss = "";int left = 0, right = 0;int count = 0, len = Integer.MAX_VALUE;Map<Character, Integer> hash1 = new HashMap<>();for (int i = 0; i < t.length(); i++) {hash1.put(t.charAt(i), hash1.getOrDefault(t.charAt(i), 0) + 1);}Map<Character, Integer> hash2 = new HashMap<>();for (; right < s.length(); right++) {char in = s.charAt(right);// 进窗口if (hash1.containsKey(in)) {hash2.put(in, hash2.getOrDefault(in, 0) + 1);// 只有当 hash2 中的该字符数量恰好等于 hash1 时才增加 countif (Objects.equals(hash2.get(in), hash1.get(in))) {count++;}}// 判断是否满足条件while (count == hash1.size()) {// 更新结果if (right - left + 1 < len) {ss = s.substring(left, right + 1);len = right - left + 1;}// 出窗口char leftChar = s.charAt(left);if (hash1.containsKey(leftChar)) {// 只有当 hash2 中的该字符数量恰好等于 hash1 时才减少 countif (Objects.equals(hash2.get(leftChar), hash1.get(leftChar))) {count--;}hash2.put(leftChar, hash2.get(leftChar) - 1);}left++;}}return ss;
}}


http://www.dtcms.com/a/339505.html

相关文章:

  • Fragment重要知识点总结
  • CloudDM 新增支持 GaussDB 与 openGauss:国产数据库管理更高效
  • OpenHarmony 之多模态输入子系统源码深度架构解析
  • Android -登录注册实践技术总结
  • 2025最新华为云国际版注册图文流程-不用绑定海外信用卡注册
  • 延时任务定时器的实现
  • Python 新工具 uv
  • 读《精益数据分析》:营收(Revenue)—— 设计可持续盈利模式
  • 【ASP.NET Core】ASP.NET Core中间件解析
  • sfc_os!SfcQueueValidationRequest函数分析之sfc_os!IsFileInQueue
  • 关闭VSCode Markdown插件在Jupyter Notebook中的自动预览
  • 在linux系统中下载Andconda
  • windows电脑对于dell(戴尔)台式的安装,与创建索引盘,系统迁移到新硬盘
  • Vim 编辑器使用指南
  • Java I/O 模型精讲:从传统BIO到高性能NIO与AIO
  • uv与conda的区别及选择指南
  • Linux软件编程:进程与线程(进程(消息队列、共享内存、信号灯))
  • 决策树二-泰坦尼克号幸存者
  • 微服务集训整理
  • AI赋能市场拓展:从智能潜力洞察到渠道精准深耕,解锁商业增长新蓝海
  • 【Proteus仿真】【51单片机】基于51单片机密码锁
  • 优秘企业智脑 AISEO 实战:如何通过多模态大模型提升搜索引擎排名?附 3 大行业案例
  • pytorch学习笔记-argparse的使用(加更版)
  • 基于SpringBoot+Vue的写真馆预约管理系统(邮箱通知、WebSocket及时通讯、协同过滤算法)
  • 哪些仪器适合对接电子实验记录本,哪些不适合?
  • Java 11中的Collections类详解
  • Web安全攻防基础
  • 什么是IP隔离?一文讲清跨境电商/海外社媒的IP隔离逻辑
  • JVM对象创建和内存分配
  • 2025年12大AI测试自动化工具