滑动窗口
滑动窗口模板
....
int left = 0, right = 0;
//len为需要滑动的窗口的长度
while(right < len) {
...
right++;
while或者if() {
left++;
}
}
题型一: 求一个字符串的字串,子串中至多有2,n个不重复的字符,或者字符串中无重复字符
题型二: 替换k个字符后最长的连续子串
题型三: 有2个字符串s和t,求s中能包含t中所有的字母
题型四: 求连续子数组的最大或者最小值
3.无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
方法一:
public int lengthOfLongestSubstring(String s) { if(s == null || s.length() == 0) return 0; int len = s.length(); int res = 1; char[] chs = s.toCharArray(); HashMap<Character,Integer> hash = new HashMap<>(); int i = 0, j = 0; while(j < len) { char ch = chs[j]; if(hash.containsKey(ch)) { i = Math.max(i,hash.get(ch) + 1); } hash.put(ch,j); j++; res = Math.max(res,j - i); } return res; }
方法二:
159.至多包含两个不同字符的最长子串
题目描述:给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t 。
示例 1:
输入: “eceba”
输出: 3
解释: t 是 “ece”,长度为3。
public int lengthOfLongestSubstringTwoDistinct(String s) { //代表当前出现的字符数量 int cnt = 0; int ans = 0; int len = s.length(); char[] chs = s.toCharArray(); int hash[] = new int[128]; int left = 0, right = 0; while (right < len) {
//说明right的元素是第一次出现 if(hash[chs[right]] == 0) { cnt++; } hash[chs[right++]]++;
//cnt代表了现在一共有的字符数 while (left < len && cnt > 2) { hash[chs[left]]--; if(hash[chs[left++]] == 0) { cnt--; } } if(len == left) break; ans = Math.max(ans,right - left); } return ans; }
424.替换后的最长重复字串
给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
注意:
字符串长度 和 k 不会超过 104。
public int characterReplacement(String s, int k) { int sLen = s.length(); //代表了目前滑动窗口的最大值 int curMax = 0; int ans = 0; //表示当前字符串中各个字符出现的次数 int[] hash = new int[26]; char[] chs = s.toCharArray(); int left = 0, right = 0; while (right < sLen) { int index = chs[right] - 'A'; hash[index]++;
//curMax表示重复的最多个数 curMax = Math.max(curMax,hash[index]); right++; //窗口左移 while(right - left - curMax > k) { hash[chs[left++] - 'A']--; } ans = Math.max(ans,right - left); } return ans; }
567.字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
76. 最小覆盖子串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。
示例
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
解法一:
public String minWindow(String s, String t) { if(t == null || t.length() == 0) return ""; int sLen = s.length(); int tLen = t.length(); char[] chs = s.toCharArray(); char[] cht = t.toCharArray(); //滑动窗口中出现元素的频率 int[] winFreq = new int[128]; //t字符串中出现元素的频率 int[] tFreq = new int[128]; int left = 0, right = 0; int distance = 0; int minLen = sLen + 1; int minLeft = -1; //更新tFreq for(int i = 0; i < tLen; i++) { tFreq[cht[i]]++; } while(right < sLen) { //如果当前右边界元素没有出现过 if(tFreq[chs[right]] == 0) { right++; continue; } if(winFreq[chs[right]] < tFreq[chs[right]]) { distance++; } winFreq[chs[right++]]++; ///说明覆盖成功 while(distance == tLen) { if((right - left) < minLen) { minLen = right - left; minLeft = left; } if(tFreq[chs[left]] == 0) { left++; continue; } if(winFreq[chs[left]] == tFreq[chs[left]]) { distance--; } winFreq[chs[left]]--; left++; } } if(minLen == sLen + 1) { return ""; } return s.substring(minLeft, minLen + minLeft); }
解法二: 对解法一做了精简,只贴出不一样的地方
while(right < sLen) { if(winFreq[chs[right]] < tFreq[chs[right]]) { distance++; } winFreq[chs[right++]]++; ///说明覆盖成功 while(left < sLen && distance == tLen) { if((right - left) < minLen) { minLen = right - left; minLeft = left; } if(winFreq[chs[left]] == tFreq[chs[left]]) { distance--; } winFreq[chs[left++]]--; } }
1234. 替换字串得到平衡字符串
有一个只含有 'Q', 'W', 'E', 'R'
四种字符,且长度为 n 的字符串。假如在该字符串中,这四个字符都恰好出现 n/4
次,那么它就是一个「平衡字符串」。
给你一个这样的字符串 s
,请通过「替换子串」的方式,使原字符串 s 变成一个「平衡字符串」。你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。
请返回待替换子串的最小可能长度。如果原字符串自身就是一个平衡字符串,则返回 0
分析:一眼看过去和滑动窗口没有关系,但是可以使其转为找到一个窗口,窗口中需要包含某些字符,从而转为最小覆盖字串的问题
public int balancedString(String s) { int sLen = s.length(); int res = sLen - 1; int left = 0, right = 0; int[] need = new int[26]; int[] cur = new int[26]; char[] chs = s.toCharArray(); int distance = 0; Arrays.fill(need, -sLen/4); for(int i = 0; i < sLen; i++) { need[chs[i] - 'A']++; }
//得到需要转换的字符串各个字符的数量 for(int i = 0; i < 26; i++) { if(need[i] > 0) distance += need[i]; } if(distance == 0) return 0; while(right < sLen) { int index = chs[right] - 'A'; if(need[index] > 0) { if(cur[index] < need[index]) { distance--; } cur[index]++; } right++; while(left < sLen && distance == 0) { res = Math.min(res,right - left); int index1 = chs[left] - 'A'; if(need[index1] > 0) { if(cur[index1] == need[index1]) { distance++; } cur[index1]--; } left++; } } return res; }
438. 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
分析:和567题一样
public List<Integer> findAnagrams(String s, String p) {
30.串联所有单词的字串
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
解法一:暴力解法
public List<Integer> findSubstring(String s, String[] words) { List<Integer> res = new ArrayList<>(); if (s == null || s.length() == 0 || words == null || words.length == 0) return res; HashMap<String, Integer> map = new HashMap<>(); int one_word = words[0].length(); int word_num = words.length; int all_len = one_word * word_num; for (String word : words) { map.put(word,map.getOrDefault(word,0) + 1); } for (int i = 0; i < s.length() - all_len + 1; i++) { String tmp = s.substring(i, i + all_len); HashMap<String,Integer> tmp_map = new HashMap<>(); for (int j = 0; j < all_len; j+=one_word) { String w = tmp.substring(j,j + one_word); if(!map.containsKey(w)) break; tmp_map.put(w,tmp_map.getOrDefault(w,0) + 1); } if((map.size() == tmp_map.size()) && map.equals(tmp_map)) res.add(i); } return res; }
解法二:滑动窗口
209.长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
public int minSubArrayLen(int s, int[] nums) {
if(nums == null || nums.length == 0) return 0;
int left = 0, right = 0;
int len = nums.length;
int minLen = len + 1;
int sum = 0;
while(right < len) {
sum += nums[right];
right++;
while(sum >= s) {
minLen = Math.min(minLen,right - left);
sum -= nums[left];
left++;
}
}
if(minLen == len + 1) {
return 0;
}
return minLen;
}
1423.可获得的最大点数
几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints
给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k
张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组 cardPoints
和整数 k
,请你返回可以获得的最大点数。
分析:这题滑动窗口用的比较巧妙,要求的是点数的最大值,我们只需要求对应连续固定滑动窗口的最小值(和209题相似),然后相减就可以了
public int maxScore(int[] cardPoints, int k) { int len = cardPoints.length; int sum = 0; int left = 0, right = 0; int curSum = 0; //代表了要维护的滑动窗口的大小 int slipWindowLen = len - k; int minSum = Integer.MAX_VALUE; for(int i = 0; i < len; i++) { sum += cardPoints[i]; } if(k == len) { return sum; } while(right < len) { curSum += cardPoints[right]; right++; //说明了滑动窗口中的元素已经达到了我们需要的值,需要移动来维护滑动窗口 if(right > slipWindowLen) { curSum -= cardPoints[left++]; } if(right >= slipWindowLen) { minSum = Math.min(minSum,curSum); } } return sum - minSum; }
1052.爱生气的老板
今天,书店老板有一家店打算试营业 customers.length
分钟。每分钟都有一些顾客(customers[i]
)会进入书店,所有这些顾客都会在那一分钟结束后离开。
在某些时候,书店老板会生气。 如果书店老板在第 i
分钟生气,那么 grumpy[i] = 1
,否则 grumpy[i] = 0
。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。
书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X
分钟不生气,但却只能使用一次。
请你返回这一天营业下来,最多有多少客户能够感到满意的数量。
示例:
输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3
输出:16
解释:
书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.
分析:要求的是X分钟不生气的最大值,可以转换为在滑动窗口里面由于不生气转换来的最大值,和1423题一样