数组——长度最小的子数组

这一篇讲滑动窗口,所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们想要的结果。循环的索引,一定是表示 滑动窗口的终止位置

滑动窗口和双指针很像,但双指针要的是两个指针所指的元素,而滑动窗口要的是两个指针之间的元素。

 这里以力扣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种水果为止。

明确后,我们需要两个变量 ij 分别记录起始位置、终止位置,一个变量 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的所有字符时,移动起始位置。

首先,需要两个变量 ij 记录起始位置、终止位置,需要一个变量 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;


版权声明:本文为Mr_guocxy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。