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这个键。
- 在HashMap中右指针对应元素的value>1的时候更新左指针
- HashMap中左指针元素的value值-1,左指针右移一位
- 直到HashMap中右指针对应元素value不大于1,就停止更新
- 继续移动右指针,将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;
}}