滑动窗口详解
滑动窗口是一种高效的数组或字符串问题解决策略,通常用于解决子数组、子字符串相关的问题,如 最小窗口、最大窗口、满足条件的子数组或子字符串等。
滑动窗口思想
滑动窗口的核心思想是将一段区间作为“窗口”,通过动态调整窗口的边界来满足问题的约束条件,避免不必要的重复计算,从而提高效率。
基本步骤
- 定义窗口的起点和终点:通常是两个指针,
left
和right
,分别表示窗口的起始位置和结束位置。 - 调整窗口:
- 扩展窗口:通过增加
right
指针来扩大窗口。 - 收缩窗口:当窗口满足某些条件时,移动
left
指针收缩窗口。
- 扩展窗口:通过增加
- 更新结果:在窗口调整的过程中,更新满足条件的结果(如最大长度、最小长度等)。
滑动窗口的适用场景
滑动窗口常用于以下问题类型:
- 固定窗口大小:如求固定大小子数组的最大和、最小和等。
- 动态窗口大小:如最长连续子数组、最短子数组、满足条件的子字符串等。
滑动窗口模板
以下是滑动窗口的一般模板:
public int slidingWindowExample(int[] nums) {
int left = 0, right = 0; // 定义窗口的左右边界
int result = 0; // 存储结果
int windowSum = 0; // 窗口中的状态(如和、计数等)
while (right < nums.length) {
// 1. 增大窗口
windowSum += nums[right];
right++;
// 2. 缩小窗口(当条件不满足时)
while (windowSum > target) {
windowSum -= nums[left];
left++;
}
// 3. 更新结果
result = Math.max(result, right - left);
}
return result;
}
滑动窗口应用实例
1. 固定大小的滑动窗口
问题:给定一个数组,求长度为 k
的连续子数组的最大和。
示例:
输入: nums = [1, 2, 3, 4, 5, 6], k = 3
输出: 15
解释: 长度为 3 的子数组最大和为 [4, 5, 6],和为 15。
代码实现:
public int maxSumSubarray(int[] nums, int k) {
int maxSum = 0, windowSum = 0;
// 初始化第一个窗口
for (int i = 0; i < k; i++) {
windowSum += nums[i];
}
maxSum = windowSum;
// 滑动窗口
for (int i = k; i < nums.length; i++) {
windowSum += nums[i] - nums[i - k];
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
时间复杂度: O ( n ) O(n) O(n),只遍历数组一次。
2. 动态窗口大小
问题:给定一个正整数数组 nums
和一个目标值 target
,找出和大于或等于 target
的最短连续子数组长度。
示例:
输入: nums = [2, 3, 1, 2, 4, 3], target = 7
输出: 2
解释: 子数组 [4, 3] 的长度最短。
代码实现:
public int minSubArrayLen(int target, int[] nums) {
int left = 0, sum = 0;
int minLength = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
// 收缩窗口,直到条件不再满足
while (sum >= target) {
minLength = Math.min(minLength, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLength == Integer.MAX_VALUE ? 0 : minLength;
}
时间复杂度: O ( n ) O(n) O(n),只遍历数组一次。
3. 无重复字符的最长子字符串
问题:给定一个字符串 s
,找出其中无重复字符的最长子字符串的长度。
示例:
输入: s = "abcabcbb"
输出: 3
解释: 无重复字符的最长子字符串是 "abc",长度为 3。
代码实现:
public int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>();
int left = 0, maxLength = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果窗口中已有字符,收缩窗口
while (window.contains(c)) {
window.remove(s.charAt(left));
left++;
}
// 添加当前字符
window.add(c);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
时间复杂度: O ( n ) O(n) O(n),每个字符最多进出窗口一次。
4. 最小覆盖子串
问题:给定两个字符串 s
和 t
,找到 s
中最小的子串,使得该子串包含 t
中所有字符。
示例:
输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
代码实现:
public String minWindow(String s, String t) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0, start = 0, minLength = Integer.MAX_VALUE;
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 更新窗口数据
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
// 收缩窗口
while (valid == need.size()) {
if (right - left < minLength) {
start = left;
minLength = right - left;
}
char d = s.charAt(left);
left++;
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
return minLength == Integer.MAX_VALUE ? "" : s.substring(start, start + minLength);
}
时间复杂度: O ( n ) O(n) O(n),窗口左右指针均最多遍历字符串一次。
滑动窗口总结
-
适用场景:
- 子数组或子字符串问题,涉及窗口条件的动态变化。
- 找到满足某些条件的最大或最小窗口。
-
核心思想:
- 通过动态调整窗口(扩大和收缩)满足条件,避免暴力法的多次重复遍历。
-
模板:
滑动窗口问题通常需要两层循环:- 外层:扩展窗口。
- 内层:在条件满足时收缩窗口并更新结果。
-
时间复杂度:
- 大多数滑动窗口算法的时间复杂度为 O ( n ) O(n) O(n),因为窗口中的元素每次只被处理一次。