变长滑动窗口
1.无重复字符的最长子串
给定一个字符串,维护一个窗口,满足窗口内的字符不重复,输出窗口的最长长度。注意的是本题窗口右侧(又名右区间)每次移动一位,而对于窗口左侧(左区间),当右区间碰到重复字符时,将左区间移动到该重复字符上一次出现的索引位置的下一位来保证窗口中字符不重复。
class Solution {
public int lengthOfLongestSubstring(String s) {
int l = 0, r = 0;
int n = s.length();
// k表示字符,v表示其最近一次出现的索引位置
Map<Character, Integer> map = new HashMap<>();
int maxLen = 0;
while(r < n) {
// 当前右区间新进来的字符t
char t = s.charAt(r);
if(map.containsKey(t)) {
// 重复了,更新左区间,左区间移动到该重复字符上一次出现的索引位置的下一位
l = Math.max(l, map.get(t) + 1);
}
// 更新/添加字符t的索引位置为r
map.put(t, r);
// 此时窗口长度是否更大,如果更大就更新
maxLen = Math.max(maxLen, r - l + 1);
// 右区间移动一位
++r;
}
return maxLen;
}
}
2.替换后的最长重复子串
给定一个字符串,维护一个窗口,满足如果窗口长度-最高频字符数量不大于k,输出满足要求的窗口的最长长度。注意的是本题窗口右侧每次移动一位,而对于窗口左侧(左区间),当窗口长度-最高频字符数量大于k,那么就需要移动一位。
class Solution {
public int characterReplacement(String s, int k) {
int l = 0, r = 0, n = s.length();
// 记录窗口内各字符频次
int[] cnt = new int[26];
int maxLen = 0, maxF = 0;;
while(r < n) {
char t = s.charAt(r);
++cnt[t - 'A'];
// 维护窗口的最高频字符的数量
maxF = Math.max(maxF, cnt[t - 'A']);
// 如果窗口长度-最高频字符数量大于k,那么说明即使k次替换也不行,需要移动左区间了
if(r - l + 1 - maxF > k) {
--cnt[s.charAt(l) - 'A'];
// 左区间右移一步
++l;
}
maxLen = Math.max(maxLen, r - l + 1);
// 右区间右移一步
++r;
}
return maxLen;
}
}
3.考试的最大困扰度
这题和2.替换后的最长重复子串不能说很像,只能说一模一样。那一题的字符串是由大写英文字符组成,这题是只由T和F组成。然后本质还是求替换后的最长重复子串。
class Solution {
public int maxConsecutiveAnswers(String answerKey, int k) {
int n = answerKey.length();
int[] arr = new int[n];
for(int i = 0; i < n; i++) {
if(answerKey.charAt(i) == 'T') {
arr[i] = 0;
} else {
arr[i] = 1;
}
}
int[] cnt = new int[2];
int l = 0, r = 0;
int maxFrW = 0, maxLen = 0;
while(r < n) {
// 字符频次更新
++cnt[arr[r]];
// 更新该窗口的最高频次字符数量
maxFrW = Math.max(maxFrW, cnt[arr[r]]);
if(r - l + 1 - maxFrW > k) {
// 左区间右移一位
--cnt[arr[l]];
++l;
}
// 右区间右移一位
maxLen = Math.max(maxLen, r - l + 1);
++r;
}
return maxLen;
}
}
4.找到字符串中所有字母异位体
给定一个字符串,维护一个窗口,满足窗口内的子字符串是给定字符串的字母异位体,输出所有满足要求的窗口的左侧(/起始)索引。注意的是本题窗口右侧每次移动一位,而对于窗口左侧(左区间),当窗口长度等于给定字符串且是其字母异位体时,那么就需要右移动一位。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int m = s.length(), n = p.length();
int[] target = new int[26];
for(int i = 0; i < n; i++) {
++target[p.charAt(i) - 'a'];
}
int[] cur = new int[26];
List<Integer> res = new ArrayList<>();
int l = 0, r = 0;
while(r < m) {
// 取出当前右区间字符
char t = s.charAt(r);
// 当前窗口的各字符出现次数
++cur[t - 'a'];
if(r - l + 1 == n) {
// 长度一样且是异位词
if(isSame(cur, target)) {
res.add(l);
}
// 左窗口移动一位
--cur[s.charAt(l) - 'a'];
++l;
}
// 右窗口移动一位
++r;
}
return res;
}
public boolean isSame(int[] cur, int[] target) {
int n = target.length;
for(int i = 0; i < n; i++) {
if(cur[i] != target[i]) {
return false;
}
}
return true;
}
}
定长滑动窗口
1.字符串的排列
假设s1的长度为m,那么维持一个长度为m的窗口,找到是否有窗口满足其中的数是s1的排列。
class Solution {
public boolean checkInclusion(String s1, String s2) {
int m = s1.length(), n = s2.length();
if(m > n) {
return false;
}
int[] c1 = new int[26];
int[] c2 = new int[26];
for(int i = 0; i < m; i++) {
++c1[s1.charAt(i) - 'a'];
++c2[s2.charAt(i) - 'a'];
}
if(isSame(c1, c2)) {
return true;
}
// 维持一个定长(长度为m)的滑动窗口
for(int r = m; r < n; r++) {
// 窗口保持右移遍历,直到满足条件
--c2[s2.charAt(r - m) - 'a'];
++c2[s2.charAt(r) - 'a'];
if(isSame(c1, c2)) {
return true;
}
}
return false;
}
public boolean isSame(int[] cur, int[] target) {
int n = target.length;
for(int i = 0; i < n; i++) {
if(cur[i] != target[i]) {
return false;
}
}
return true;
}
}
2.可获得的最大点数
假设cardPoints的长度为n,那么维持一个长度为n-k的窗口,找到窗口中数字和最小的窗口,那么除去窗口外的其他点数和就是可获得的最大点数。
class Solution {
public int maxScore(int[] cardPoints, int k) {
int zsum = Arrays.stream(cardPoints).sum();
int n = cardPoints.length;
int ws = n - k;
// 维持一个长度为n-k的滑动窗口,找到窗口内和最小的
int minSum = 0, sum = 0;
for(int i = 0; i < ws; i++) {
minSum += cardPoints[i];
sum += cardPoints[i];
}
// 窗口整体右移
for(int i = ws; i < n; i++) {
sum -= cardPoints[i - ws];
sum += cardPoints[i];
minSum = Math.min(minSum, sum);
}
return zsum - minSum;
}
}