目录
滑动窗口是什么
滑动窗口技术是一种强大的算法策略,主要用于解决涉及连续子数组或子字符串的问题,尤其是当这些问题要求优化时间复杂度时。它非常适合于解决以下类型的题型:
-
固定长度的窗口问题:
- 当要求处理长度固定的连续子数组或子字符串的问题时,滑动窗口可以按照固定的长度向前移动,计算每个窗口的目标值。例如,计算每个长度为k的连续子数组的平均值。
-
可变长度的窗口问题:
- 对于那些没有固定长度要求的子数组或子字符串问题,滑动窗口技术尤其有效。窗口的大小根据当前的需求动态调整,这类问题包括但不限于:
- 找到包含(或不包含)特定元素的最小(或最大)子数组或子字符串。
- 计算满足某些条件的最长或最短子数组或子字符串的长度。
- 找到所有满足特定条件的子数组或子字符串。
- 对于那些没有固定长度要求的子数组或子字符串问题,滑动窗口技术尤其有效。窗口的大小根据当前的需求动态调整,这类问题包括但不限于:
-
计数问题:
- 滑动窗口可以用来计数,例如,计算一个字符串中包含所有字符的最短子字符串的数量。
-
求和问题:
- 当需要找出数组中的子数组,使其和等于、小于或大于给定值时,滑动窗口可以有效地进行处理。
-
字符处理问题:
- 滑动窗口经常用于处理字符串问题,如计算不含重复字符的最长子串的长度,或找到包含所有给定字符的最短子串。
-
双指针问题的变体:
- 某些双指针问题也可以视为滑动窗口问题的变体,特别是当问题涉及到连续子序列时。
滑动窗口技术之所以强大,是因为它利用了窗口的边界来减少不必要的计算,通过只在窗口的一侧添加或删除元素,它能够在不重新遍历整个数组或字符串的情况下,快速更新窗口内的信息。这种方法通常能将时间复杂度从O(n^2)优化到O(n),使其成为解决这类问题的高效策略。
Leetcode 3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串的长度。
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
import java.util.HashMap;
import java.util.Map;
class Solution {
public int lengthOfLongestSubstring(String s) {
// 使用HashMap来存储字符及其最后一次出现的索引
Map<Character, Integer> map = new HashMap<>();
// 初始化滑动窗口的起始位置和最大长度
int start = 0, maxLength = 0;
// 遍历字符串
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果当前字符已存在于HashMap中,说明之前出现过,需要更新滑动窗口的起始位置
// 新的起始位置是当前字符上一次出现位置的下一个位置与当前start的较大值
// 这是为了保证窗口只向前移动,避免回退
if(map.containsKey(c)){
start = Math.max(map.get(c) + 1, start);
}
// 更新当前字符的索引
map.put(c, i);
// 计算当前无重复字符子串的长度(i - start + 1),并更新最大长度
maxLength = Math.max(i - start + 1, maxLength);
}
// 返回最大长度
return maxLength;
}
}
-
HashMap (
map
): 用于存储遍历过程中字符及其最后一次出现的索引。这样我们可以快速检查当前字符是否已经在之前出现过,以及它的上一次出现位置。 -
滑动窗口 (
start
,i
):start
变量表示当前考虑的不含重复字符子串的起始位置,i
表示遍历到的当前位置,它们共同定义了滑动窗口的边界。 -
更新窗口起始位置: 如果当前字符
c
已经在之前的位置出现过(即map
中已有c
的记录),则需要更新start
以确保窗口内的字符串不包含重复字符。start
更新为map.get(c) + 1
(即当前字符上一次出现位置的下一个位置)和当前start
的较大值,以保证窗口始终向前滑动,不回退。 -
更新最大长度 (
maxLength
): 在每次迭代中,都会计算当前窗口的长度(i - start + 1
),并与已知的最大长度比较,更新最大长度。 -
返回结果: 遍历完成后,
maxLength
即为所求的最长无重复字符子串的长度。
重点:start的更新——如何防止滑动窗口回退
假设我们有字符串 s = "tmsmartz"
,我们按顺序处理字符串以找到不含有重复字符的最长子串。
-
开始时,
start = 0
, 我们逐个处理字符并将它们及其索引存储在HashMap中。- 当
end = 3
, 字符串为"tmsm"
,此时我们遇到了重复字符 'm'。map.get('m')
返回 1,因为 'm' 最初出现在索引 1 的位置。根据直接跳转的逻辑,我们会将start
设置为map.get('m') + 1 = 2
。
- 当
-
接下来, 我们继续移动
end
,直到end = 6
,此时字符串为"tmsmart"
,遇到了重复字符 't'。map.get('t')
返回 0,因为 't' 最初出现在索引 0 的位置。
现在,如果我们按照直接跳转到 map.get('t') + 1
的逻辑,即 1
,这实际上比当前的 start
(在第一步后为 2
)还要小。如果我们更新 start
为 1
,那么窗口就会回退到包含两个 'm' 的位置,从 "smart"
回退到 "tms"
,这显然是错误的,因为我们已经移除了第一个 'm' 来处理第二个 'm' 的重复情况。
为了避免这种回退,我们使用 start = Math.max(map.get(currentChar) + 1, start)
。所以在处理第二个 't' 时,即使 map.get('t') + 1 = 1
,我们也会保持 start = 2
,因为 Math.max(1, 2) = 2
。这样确保了窗口只会向前移动或保持不变,不会发生回退,保持算法的正确性。