当前位置:   article > 正文

数组——长度最小的子数组_最小循环数组

最小循环数组

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

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

 这里以力扣209.长度最小的子数组为例。

Carl哥的代码随想录中,这题的关键是确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口内,就是我们所需要的元素。这里是窗口内元素的和。

如何移动窗口的结束位置?循环的索引,一定是表示滑动窗口的终止位置。当窗口内不满足我们所需要的元素时,就得移动窗口的结束位置(扩大窗口)。这里,随着循环索引,当窗口内元素之和小于题目给出的值s时,索引继续++,也就是窗口的结束位置需要移动。

如何移动窗口的起始位置?当窗口内满足我们所需要的元素时,就得移动窗口的起始位置(缩小窗口)。题目要求 子数组元素之和大于给定的值s,因此,子数组元素之和大于给定的值s时,起始位置需要移动。

通过不断移动结束位置、起始位置、结束位置、起始位置、……得到我们想要的结果。

代码如下:

  1. int i = 0; // 起始位置
  2. int j = 0; // 结束位置
  3. int sum = 0; // 窗口内元素之和
  4. int len = Integer.MAX_VALUE; // 最小长度
  5. for (j = 0; j < nums.length; j++) { // 移动结束位置
  6. sum += nums[j];
  7. while (sum >= target) { // 窗口内元素满足要求
  8. len = Math.min(len, j - i + 1); // 记录较小长度
  9. sum -= nums[i++]; // 移动起始位置
  10. }
  11. }
  12. 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

  1. int i = 0; // 起始位置
  2. int j = 0; // 终止位置
  3. int current = fruits[0]; // 窗口内离终止位置最近的水果,第1种水果
  4. int last = -1; // 窗口内第2种水果,初始为-1代表无
  5. int index = 0; // current代表的水果的左边界
  6. int maxLength = 0;
  7. for (j = 0; j < fruits.length; j++) {
  8. if (current == fruits[j]) { // 终止位置是第1种水果
  9. } else if (last == fruits[j] || last == -1) { // 终止位置是第2种水果
  10. last = current; // 交换,让第1种水果是离终止位置最近的水果
  11. current = fruits[j];
  12. index = j; // 更新第1种水果的左边界
  13. } else { // 终止位置是第3种水果,缩小窗口
  14. last = current;
  15. current = fruits[j];
  16. i = index; // 起始位置移动
  17. index = j; // 更新第1种水果的左边界
  18. }
  19. maxLength = Math.max(maxLength, j - i + 1);
  20. }
  21. 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个数字一样时,会将这两个数放在两个篮子里,而它们本来属于同一个篮子。修改之后如下:

  1. int len = fruits.size();
  2. int l = 0, r = 0; // 滑动窗口的起始、终止
  3. int l_kind = -1, r_kind = -1; // 两种水果,r_kind指相对位于右侧的水果
  4. int index = 0; // 滑动窗口缩小时,l移动至的位置
  5. int result = 0; // 记录最长窗口长度
  6. while (r < len) {
  7. if (fruits[r] == r_kind) { // 当前指向的水果和r_kind是同一种
  8. } else if (fruits[r] == l_kind) { // 当前指向的水果和l_kind是同一种,
  9. l_kind = r_kind; // 由于具有相对位置,需要交换l_kind和r_kind,
  10. r_kind = fruits[r]; // 并更新index
  11. index = r;
  12. } else {
  13. if (r_kind == -1) { // 初始篮子中没有水果
  14. r_kind = fruits[r];
  15. } else if (l_kind == -1) { // 篮子中有一种水果,当前指向的水果是第二种
  16. l_kind = r_kind;
  17. r_kind = fruits[r];
  18. index = r;
  19. } else { // 出现第三种水果
  20. l_kind = r_kind;
  21. r_kind = fruits[r];
  22. result = max(result, r - l); // 计算窗口长度
  23. l = index; // 缩小窗口
  24. index = r;
  25. }
  26. }
  27. r++;
  28. }
  29. result = max(result, r - l);
  30. return result;

最小覆盖子串

窗口内,含有字符串t中的所有字符。

如何移动终止位置?当窗口内没有包含字符串t的所有字符时,移动终止位置。

如何移动起始位置?当窗口内包含字符串t的所有字符时,移动起始位置。

首先,需要两个变量 ij 记录起始位置、终止位置,需要一个变量 valid 记录窗口内属于字符串t的有效字符个数,需要一个变量 length 记录最小长度,一个变量 ans 记录最小覆盖子串。

有效字符:例如字符串t为“abb”,只需要两个’b',那么‘a'、'b'、'b'、’b'中,两个‘b'是有效字符,剩下一个不是。

如何判断字符串t里的字符有没有包含在窗口内?使用map1记录字符串t的字符及其个数,再使用一个map2记录窗口内属于字符串t的字符及其个数。

  1. Map<Character, Integer> need = new HashMap<>(); // 字符串t中的字符及个数
  2. Map<Character, Integer> window = new HashMap<>(); // 窗口中所包含t中的字符及个数
  3. for (int i = 0; i < t.length(); i++)
  4. need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
  5. int i = 0; // 起始位置
  6. int j = 0; // 终止位置
  7. int valid = 0; // 窗口内所包含t中的有效字符个数
  8. int length = Integer.MAX_VALUE; // 窗口长度
  9. String ans = ""; // 子字符串
  10. for (j = 0; j < s.length(); j++) {
  11. if (need.containsKey(s.charAt(j))) { // 终止位置是t中的字符
  12. window.put(s.charAt(j), window.getOrDefault(s.charAt(j), 0) + 1);
  13. if (need.get(s.charAt(j)) >= window.get(s.charAt(j)))
  14. valid++; // 比较字符个数,是有效字符则valid++
  15. }
  16. while (valid == t.length()) { // 窗口内包含t中所有字符,起始位置移动
  17. if (j - i + 1 < length) {
  18. length = j - i + 1;
  19. ans = s.substring(i, j + 1); // 更新最小子字符串
  20. }
  21. if (need.containsKey(s.charAt(i))) { // 起始位置是t中的字符
  22. window.put(s.charAt(i), window.get(s.charAt(i)) - 1);
  23. if (need.get(s.charAt(i)) > window.get(s.charAt(i)))
  24. valid--; // 比较字符个数,是有效字符则valid--
  25. }
  26. i++; // 移动起始位置
  27. }
  28. }
  29. return ans;

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

同样,时隔4个月,用C++代替Java重做 76.最小覆盖子串。

代码与上面的基本一致,区别在于:不在循环体内不断计算、更新子串,而是在循环体在计算一次子串。【这里算是一个坑吧,C++代码在循环体内更新子串时,最后一个测试样例过不了,会超出时间限制。试过简化到只用一个map,也会超出时间限制。】

  1. map<char, int> schar, tchar;
  2. for (int i = 0; i < t.length(); i++)
  3. tchar[t[i]] = tchar[t[i]] + 1;
  4. int l = 0, r = 0;
  5. int valid = 0;
  6. int minLen = INT_MAX;
  7. int left = 0;
  8. string result = "";
  9. while (r < s.length()) {
  10. if (tchar.count(s[r]) != 0) {
  11. schar[s[r]]++;
  12. if (schar[s[r]] <= tchar[s[r]])
  13. valid++;
  14. }
  15. while (valid == t.length()) {
  16. if (tchar.count(s[l]) != 0) {
  17. schar[s[l]]--;
  18. if (schar[s[l]] < tchar[s[l]])
  19. valid--;
  20. }
  21. if (minLen > r - l + 1) {
  22. minLen = r - l + 1;
  23. left = l;
  24. }
  25. l++;
  26. }
  27. r++;
  28. }
  29. return (minLen == INT_MAX) ? result : s.substr(left, minLen);

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/543292
推荐阅读
相关标签
  

闽ICP备14008679号