1. 滑动窗口基本写法
滑动窗口需要确定三个条件
- 窗口内是什么
- 如何移动窗口的起始位置
- 如何移动窗口的结束位置
例题 :209 长度最小的子数组
本题窗口内时元素的和,当元素和大于目标值就需要缩小窗口范围尝试搜索更优的解
注意:这里要先对当前最优结果进行保存,再去尝试找更优的解
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int sum = 0;
int i = 0,res = Integer.MAX_VALUE;
//以尾指针作为循环变量
for(int j = 0;j < nums.length;j++){
sum += nums[j];
//如果sum之和大于了target,就要缩小窗口的大小,同时与之前保存的长度进行比较,确认保存最小的长度
while(sum >= target){
//记录子数组的长度
// int subLength = j - i + 1;
// res = res > subLength ? subLength : res;
//可以直接使用内置函数
res = Math.min(res,j-i+1);
//缩小窗口大小
sum -= nums[i++];
}
}
return res == Integer.MAX_VALUE ? 0 : res;
}
}
2.边长滑动窗口与哈希Map结合
数组类型
904 水果成篮
本题的窗口内保存的是水果的种类数,只要超过两种就需要缩小窗口大小,即移动左边界
注意:这里需要先拿出水果,再去保存最优值。因为水果种类数等于2时,下一个要遍历的值可能还是当前篮子种类型的水果,不能直接开始拿水果的操作。
class Solution {
public int totalFruit(int[] fruits) {
//定义变量保存结果
int res = 0;
//用来保存水果的数量
Map<Integer,Integer> count = new HashMap<>();
//i为左指针,j为右指针
int i = 0;
for(int j = 0;j < fruits.length;j++){
//开始装水果
count.put(fruits[j],count.getOrDefault(fruits[j],0)+1);
//水果种类超过两种时
while(count.size() > 2){
//拿出水果
count.put(fruits[i],count.getOrDefault(fruits[i],0)-1);
//如果水果个数为0,就移除该类型的水果
if(count.get(fruits[i]) == 0){
count.remove(fruits[i]);
}
//左指针向后移
i++;
}
//尽可能让滑动窗口大,这个时候说明某个类型的水果很多
res = Math.max(res,j-i+1);
}
return res;
}
}
面试题 17.18. 最短超串
本题与下文将要介绍的字符串类型的两题类似,唯一不同的就是返回值,这里需要更加注意边界条件的判断
class Solution {
public int[] shortestSeq(int[] big, int[] small) {
//如果big的长度小于small的长度,那就不可能满足条件
// if(big.length < small.length) return new int[]{};
//定义一个map存储small需要的字符种类和对应的数量
Map<Integer,Integer> cnt = new HashMap<>();
//遍历small数组保存map的值
for(int i = 0;i < small.length;i++){
cnt.put(small[i],cnt.getOrDefault(small[i],0)+1);
}
//定义除了滑动窗口外还需要匹配的数字
int need = small.length;
int n = big.length;
//定义一个较大值
int minLen = n + 1;
//定义滑动窗口左右指针
int left = 0,right;
int start = 0,end = -1;
//开始滑动窗口啦
for(right = 0;right < n;right++){
//扩大右边界,同时修改map和还需要的字符
int ele = big[right];
if(cnt.containsKey(ele)){
if(cnt.get(ele) > 0){//对该数字还有需求
need--;
}
cnt.put(ele,cnt.get(ele)-1);
}
//循环缩小左边界,需要在need为0,即对所有数字都没有需求的时候开始缩小左边界,获取最小长度
while(need == 0){
ele = big[left];
//保存当前最优值
if(right - left + 1 < minLen){
minLen = right - left + 1;
start = left;
end = right;
}
//缩小边界尝试找更短
if(cnt.containsKey(ele)){
if(cnt.get(ele) >= 0){//还有需求,
need++;
}
cnt.put(ele,cnt.get(ele)+1);
}
left++;
}
}
return start <= end ? new int[]{start,end} : new int[]{};//注意边界条件的判断
}
}
字符串类型
只要字符串一种包含字符串二的所有字符,然后长度最短就可以,字符串二不一定是字符串一的子串
76 最小覆盖子串
剑指 Offer II 017. 含有所有字符的最短字符串
这里窗口内保存的是 是否s已经覆盖t,如果已经覆盖了再收缩边界。
注意:t字符串可能会重复,所以哈希map不仅要保存当前需要的字符种类,还要保存当前需要字符的数量。如果哈希map值为负数,说明已经不需要该字符了。
class Solution {
public String minWindow(String s, String t) {
//如果s的长度小于t的长度,那就不可能有覆盖字串
if(s.length() < t.length()) return "";
//定义一个map存储t需要的字符种类和对应的数量
Map<Character,Integer> cnt = new HashMap<>();
//遍历t字符串保存map的值
for(int i = 0;i < t.length();i++){
cnt.put(t.charAt(i),cnt.getOrDefault(t.charAt(i),0)+1);
}
//定义除了滑动窗口外还需要匹配的字符的数目
int need = t.length();
int n = s.length();
//定义一个较大值
int minLen = n + 1;
//定义滑动窗口左右指针
int left = 0,right;
int start = 0,end = -1;
//开始滑动窗口啦
for(right = 0;right < n;right++){
//扩大右边界,同时修改map和还需要的字符
char ch = s.charAt(right);
if(cnt.containsKey(ch)){
if(cnt.get(ch) > 0){//对该字符还有需求
need--;
}
cnt.put(ch,cnt.get(ch)-1);
}
//循环缩小左边界,需要在need为0,即对所有字符都没有需求的时候开始缩小左边界,获取最小长度
while(need == 0){
ch = s.charAt(left);
//保存当前最优值
if(right - left + 1 < minLen){
minLen = right - left + 1;
start = left;
end = right;
}
//缩小边界尝试找更短
if(cnt.containsKey(ch)){
if(cnt.get(ch) >= 0){//还有需求,
need++;
}
cnt.put(ch,cnt.get(ch)+1);
}
left++;
}
}
return s.substring(start,end+1);
}
}
3.定长的滑动窗口------异位词
判断第一个字符串的排列之一是第二个字符串的子串。
567 字符串的排列
剑指 Offer II 014. 字符串中的变位词
维护一个长度为s1.length()的窗口,如果need等于0,说明当前窗口中的元素正好与s1相同
class Solution {
public boolean checkInclusion(String s1, String s2) {
if(s2.length() < s1.length()) return false;
//定义一个map存储t需要的字符种类和对应的数量
Map<Character,Integer> cnt = new HashMap<>();
for(int i = 0;i < s1.length();i++){
cnt.put(s1.charAt(i),cnt.getOrDefault(s1.charAt(i),0)+1);
}
//定义除了滑动窗口外还需要匹配的字符的数目
int need = s1.length();
int n = s2.length();
//定义滑动窗口左右指针
int left,right;
//开始滑动窗口啦,这里的滑动窗口是一个定长的滑动窗口,其实就是每次判断一个不包含左边界,只包含右边界的窗口内的值是否和s1的值相等
for(right = 0;right < n;right++){
char ch = s2.charAt(right);
if(cnt.containsKey(ch)){
if(cnt.get(ch) > 0){//对该字符还有需求
need--;
}
cnt.put(ch,cnt.get(ch)-1);
}
left = right - s1.length();
if(left >= 0){
ch = s2.charAt(left);
if(cnt.containsKey(ch)){
if(cnt.get(ch) >= 0){
need++;
}
cnt.put(ch,cnt.get(ch)+1);
}
}
if(need == 0) return true;
}
return false;
}
}
4.定长滑动窗口------异位词的位置
438. 找到字符串中所有字母异位词
剑指 Offer II 015. 字符串中的所有变位词
维护一个长度为s1.length()的窗口,如果need等于0,说明当前窗口中的元素正好与s1相同,此时返回left+1
,因为窗口是不包含左端点的。
class Solution {
public List<Integer> findAnagrams(String s, String t) {
List<Integer> res = new ArrayList<>();
//如果s的长度小于t的长度,那就不可能有覆盖字串
//if(s.length() < t.length()) return res;
//定义一个map存储t需要的字符种类和对应的数量
Map<Character,Integer> cnt = new HashMap<>();
//遍历t字符串保存map的值
for(int i = 0;i < t.length();i++){
cnt.put(t.charAt(i),cnt.getOrDefault(t.charAt(i),0)+1);
}
//定义除了滑动窗口外还需要匹配的字符的数目
int need = t.length();
int n = s.length();
//定义滑动窗口左右指针
int left = 0,right;
//开始滑动窗口啦
for(right = 0;right < n;right++){
//扩大右边界,同时修改map和还需要的字符
char ch = s.charAt(right);
if(cnt.containsKey(ch)){
if(cnt.get(ch) > 0){//对该字符还有需求
need--;
}
cnt.put(ch,cnt.get(ch)-1);
}
left = right - t.length();
if(left >= 0){
ch = s.charAt(left);
if(cnt.containsKey(ch)){
if(cnt.get(ch) >= 0){
need++;
}
cnt.put(ch,cnt.get(ch)+1);
}
}
if(need == 0) res.add(left+1);
}
return res;
}
}