这一篇讲滑动窗口,所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们想要的结果。循环的索引,一定是表示 滑动窗口的终止位置。
滑动窗口和双指针很像,但双指针要的是两个指针所指的元素,而滑动窗口要的是两个指针之间的元素。
这里以力扣209.长度最小的子数组为例。
Carl哥的代码随想录中,这题的关键是确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口内,就是我们所需要的元素。这里是窗口内元素的和。
如何移动窗口的结束位置?循环的索引,一定是表示滑动窗口的终止位置。当窗口内不满足我们所需要的元素时,就得移动窗口的结束位置(扩大窗口)。这里,随着循环索引,当窗口内元素之和小于题目给出的值s时,索引继续++,也就是窗口的结束位置需要移动。
如何移动窗口的起始位置?当窗口内满足我们所需要的元素时,就得移动窗口的起始位置(缩小窗口)。题目要求 子数组元素之和大于给定的值s,因此,子数组元素之和大于给定的值s时,起始位置需要移动。
通过不断移动结束位置、起始位置、结束位置、起始位置、……得到我们想要的结果。
代码如下:
int i = 0; // 起始位置
int j = 0; // 结束位置
int sum = 0; // 窗口内元素之和
int len = Integer.MAX_VALUE; // 最小长度
for (j = 0; j < nums.length; j++) { // 移动结束位置
sum += nums[j];
while (sum >= target) { // 窗口内元素满足要求
len = Math.min(len, j - i + 1); // 记录较小长度
sum -= nums[i++]; // 移动起始位置
}
}
return (len == Integer.MAX_VALUE) ? 0 : len; // 若len不曾改变,说明没有满足要求的子数组代码随想录里推荐的相似题目有904.水果成篮,76.最小覆盖子串。
水果成篮:
还是以三个关键点为切入点进行分析。
窗口内,就是我们所需要的2种水果数量。
如何移动终止位置?当终止位置不是第3种水果时,移动终止位置。
如何移动起始位置?当终止位置是第3种水果时,起始位置开始移动,移动到窗口中只剩2种水果为止。
明确后,我们需要两个变量 i、j 分别记录起始位置、终止位置,一个变量 current 记录离终止位置最近的水果(记为第1种水果),一个变量 last 记录第2种水果(离终止位置较远),一个变量 maxLength记录最大长度。
这里将第1种水果记为离终止位置最近的水果,是为了有序,不然一会current是第1种水果,一会last是第1种水果,在循环过程中就会混乱。
最后,当终止位置是第3种水果时,起始位置移动到哪?应该移动到current水果的左边界。如果从终止位置往前循环查找左边界,总时间复杂度最坏会是O(n^2),不好。那么,只能在终止位置移动时记录current水果的左边界,需要一个变量 index。
int i = 0; // 起始位置
int j = 0; // 终止位置
int current = fruits[0]; // 窗口内离终止位置最近的水果,第1种水果
int last = -1; // 窗口内第2种水果,初始为-1代表无
int index = 0; // current代表的水果的左边界
int maxLength = 0;
for (j = 0; j < fruits.length; j++) {
if (current == fruits[j]) { // 终止位置是第1种水果
} else if (last == fruits[j] || last == -1) { // 终止位置是第2种水果
last = current; // 交换,让第1种水果是离终止位置最近的水果
current = fruits[j];
index = j; // 更新第1种水果的左边界
} else { // 终止位置是第3种水果,缩小窗口
last = current;
current = fruits[j];
i = index; // 起始位置移动
index = j; // 更新第1种水果的左边界
}
maxLength = Math.max(maxLength, j - i + 1);
}
return maxLength;最小覆盖子串:
窗口内,含有字符串t中的所有字符。
如何移动终止位置?当窗口内没有包含字符串t的所有字符时,移动终止位置。
如何移动起始位置?当窗口内包含字符串t的所有字符时,移动起始位置。
首先,需要两个变量 i、j 记录起始位置、终止位置,需要一个变量 valid 记录窗口内属于字符串t的有效字符个数,需要一个变量 length 记录最小长度,一个变量 ans 记录最小覆盖子串。
有效字符:例如字符串t为“abb”,只需要两个’b',那么‘a'、'b'、'b'、’b'中,两个‘b'是有效字符,剩下一个不是。
如何判断字符串t里的字符有没有包含在窗口内?使用map1记录字符串t的字符及其个数,再使用一个map2记录窗口内属于字符串t的字符及其个数。
Map<Character, Integer> need = new HashMap<>(); // 字符串t中的字符及个数
Map<Character, Integer> window = new HashMap<>(); // 窗口中所包含t中的字符及个数
for (int i = 0; i < t.length(); i++)
need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
int i = 0; // 起始位置
int j = 0; // 终止位置
int valid = 0; // 窗口内所包含t中的有效字符个数
int length = Integer.MAX_VALUE; // 窗口长度
String ans = ""; // 子字符串
for (j = 0; j < s.length(); j++) {
if (need.containsKey(s.charAt(j))) { // 终止位置是t中的字符
window.put(s.charAt(j), window.getOrDefault(s.charAt(j), 0) + 1);
if (need.get(s.charAt(j)) >= window.get(s.charAt(j)))
valid++; // 比较字符个数,是有效字符则valid++
}
while (valid == t.length()) { // 窗口内包含t中所有字符,起始位置移动
if (j - i + 1 < length) {
length = j - i + 1;
ans = s.substring(i, j + 1); // 更新最小子字符串
}
if (need.containsKey(s.charAt(i))) { // 起始位置是t中的字符
window.put(s.charAt(i), window.get(s.charAt(i)) - 1);
if (need.get(s.charAt(i)) > window.get(s.charAt(i)))
valid--; // 比较字符个数,是有效字符则valid--
}
i++; // 移动起始位置
}
}
return ans;