赞
踩
这一篇讲滑动窗口,所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们想要的结果。循环的索引,一定是表示 滑动窗口的终止位置。
滑动窗口和双指针很像,但双指针要的是两个指针所指的元素,而滑动窗口要的是两个指针之间的元素。
这里以力扣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;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
这里记录一下后面时隔4个月后用C++,忘记细节的情况下按照滑动窗口思想做这题的思路:
首先是窗口的起始、终止指针;两个变量记录两种水果的种类(由于窗口长度就是篮子里水果的数量,因此不需要记录水果数量),初始化为-1表示未放水果;一个变量记录窗口缩小时的位置;一个变量记录结果。
在循环中,遇到r_kind一致的水果不做任何操作;遇到 l_kind一致的水果交换 l_kind和r_kind以保持左右的相对位置;遇到和 l_kind与r_kind都不一致的水果,则记录窗口长度,缩小窗口。到此为止,都没问题。问题出现在我将 if(r_kind == -1)、if (l_kind == -1),即篮子为空的判断,写在了循环体最前面,导致像 fruits={3, 3, ...} 这种数组前2个数字一样时,会将这两个数放在两个篮子里,而它们本来属于同一个篮子。修改之后如下:
- int len = fruits.size();
- int l = 0, r = 0; // 滑动窗口的起始、终止
- int l_kind = -1, r_kind = -1; // 两种水果,r_kind指相对位于右侧的水果
- int index = 0; // 滑动窗口缩小时,l移动至的位置
- int result = 0; // 记录最长窗口长度
- while (r < len) {
- if (fruits[r] == r_kind) { // 当前指向的水果和r_kind是同一种
-
- } else if (fruits[r] == l_kind) { // 当前指向的水果和l_kind是同一种,
- l_kind = r_kind; // 由于具有相对位置,需要交换l_kind和r_kind,
- r_kind = fruits[r]; // 并更新index
- index = r;
- } else {
- if (r_kind == -1) { // 初始篮子中没有水果
- r_kind = fruits[r];
- } else if (l_kind == -1) { // 篮子中有一种水果,当前指向的水果是第二种
- l_kind = r_kind;
- r_kind = fruits[r];
- index = r;
- } else { // 出现第三种水果
- l_kind = r_kind;
- r_kind = fruits[r];
- result = max(result, r - l); // 计算窗口长度
- l = index; // 缩小窗口
- index = r;
- }
- }
- r++;
- }
- result = max(result, r - l);
- return result;
最小覆盖子串:
窗口内,含有字符串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;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
同样,时隔4个月,用C++代替Java重做 76.最小覆盖子串。
代码与上面的基本一致,区别在于:不在循环体内不断计算、更新子串,而是在循环体在计算一次子串。【这里算是一个坑吧,C++代码在循环体内更新子串时,最后一个测试样例过不了,会超出时间限制。试过简化到只用一个map,也会超出时间限制。】
- map<char, int> schar, tchar;
- for (int i = 0; i < t.length(); i++)
- tchar[t[i]] = tchar[t[i]] + 1;
- int l = 0, r = 0;
- int valid = 0;
- int minLen = INT_MAX;
- int left = 0;
- string result = "";
- while (r < s.length()) {
- if (tchar.count(s[r]) != 0) {
- schar[s[r]]++;
- if (schar[s[r]] <= tchar[s[r]])
- valid++;
- }
- while (valid == t.length()) {
- if (tchar.count(s[l]) != 0) {
- schar[s[l]]--;
- if (schar[s[l]] < tchar[s[l]])
- valid--;
- }
- if (minLen > r - l + 1) {
- minLen = r - l + 1;
- left = l;
- }
- l++;
- }
- r++;
- }
- return (minLen == INT_MAX) ? result : s.substr(left, minLen);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。