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

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

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

 这里以力扣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;

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

这里记录一下后面时隔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的所有字符时,移动起始位置。

首先,需要两个变量 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;

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

同样,时隔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);

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值