滑动窗口核心思想
滑动窗口是一种双指针技巧,主要用于解决数组/字符串的连续子区间问题。通过维护一个可滑动的窗口,在O(n)时间复杂度内解决问题。
基本框架:
java
int left = 0, right = 0;
while (right < n) {
// 1. 进窗口
// 2. 判断条件
while (条件满足) {
// 3. 更新结果
// 4. 出窗口
left++;
}
right++;
}
题目详解
1. 长度最小的子数组
问题:找到和≥target的最短连续子数组
java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0, right = 0;
int n = nums.length;
int sum = 0;
int minLen = Integer.MAX_VALUE;
for (; right < n; right++) {
sum += nums[right]; // 进窗口
while (sum >= target) { // 判断条件
minLen = Math.min(minLen, right - left + 1); // 更新结果
sum -= nums[left]; // 出窗口
left++;
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
关键点:
- 右指针扩大窗口,左指针缩小窗口
- 在满足条件时立即更新结果
2. 无重复字符的最长子串
问题:找到不含重复字符的最长子串
java
class Solution {
public int lengthOfLongestSubstring(String s) {
int[] hash = new int[128]; // ASCII码范围
char[] arr = s.toCharArray();
int left = 0, right = 0;
int maxLen = 0;
int n = arr.length;
for (; right < n; right++) {
hash[arr[right]]++; // 进窗口
while (hash[arr[right]] > 1) { // 出现重复
hash[arr[left]]--; // 出窗口
left++;
}
maxLen = Math.max(maxLen, right - left + 1); // 更新结果
}
return maxLen;
}
}
优化:使用数组代替HashMap,性能更好
3. 最大连续1的个数
问题:最多翻转k个0,找到最长连续1
java
class Solution {
public int longestOnes(int[] nums, int k) {
int left = 0, right = 0;
int zeroCount = 0;
int maxLen = 0;
int n = nums.length;
for (; right < n; right++) {
if (nums[right] == 0) {
zeroCount++; // 进窗口
}
while (zeroCount > k) { // 超过翻转限制
if (nums[left] == 0) {
zeroCount--; // 出窗口
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1); // 更新结果
}
return maxLen;
}
}
技巧:把0的个数当作"资源",不超过k即可
4. 将x减到0的最小操作数
问题:从两端移除元素使和等于x
java
class Solution {
public int minOperations(int[] nums, int x) {
int total = 0;
for (int num : nums) total += num;
int target = total - x; // 中间连续子数组的和
if (target < 0) return -1;
int left = 0, right = 0;
int currentSum = 0;
int maxLen = -1;
int n = nums.length;
for (; right < n; right++) {
currentSum += nums[right]; // 进窗口
while (currentSum > target && left <= right) {
currentSum -= nums[left]; // 出窗口
left++;
}
if (currentSum == target) {
maxLen = Math.max(maxLen, right - left + 1); // 更新结果
}
}
return maxLen == -1 ? -1 : n - maxLen;
}
}
思路转换:移除两边 → 保留中间连续子数组
5. 水果成篮
问题:最多收集两种水果,求最大数量
java
class Solution {
public int totalFruit(int[] fruits) {
int[] hash = new int[100001]; // 题目数据范围
int left = 0, right = 0;
int typeCount = 0;
int maxLen = 0;
int n = fruits.length;
for (; right < n; right++) {
if (hash[fruits[right]] == 0) {
typeCount++; // 新水果类型
}
hash[fruits[right]]++; // 进窗口
while (typeCount > 2) { // 超过两种水果
hash[fruits[left]]--; // 出窗口
if (hash[fruits[left]] == 0) {
typeCount--;
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1); // 更新结果
}
return maxLen;
}
}
关键:维护水果类型计数
6. 找到字符串中所有字母的异位词
问题:在s中找到p的所有异位词起始位置
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
int[] target = new int[26];
int[] window = new int[26];
// 统计目标字符串
for (char c : p.toCharArray()) {
target[c - 'a']++;
}
int left = 0, right = 0;
int validCount = 0;
int sLen = s.length(), pLen = p.length();
for (; right < sLen; right++) {
char in = s.charAt(right);
window[in - 'a']++; // 进窗口
// 有效字符判断
if (window[in - 'a'] <= target[in - 'a']) {
validCount++;
}
// 窗口大小固定为p的长度
if (right - left + 1 > pLen) {
char out = s.charAt(left);
if (window[out - 'a'] <= target[out - 'a']) {
validCount--;
}
window[out - 'a']--; // 出窗口
left++;
}
// 找到异位词
if (validCount == pLen) {
result.add(left);
}
}
return result;
}
}
技巧:固定窗口大小,统计有效字符数
7. 最小覆盖子串
问题:在s中找到包含t所有字符的最小子串
java
class Solution {
public String minWindow(String s, String t) {
int[] target = new int[128];
int[] window = new int[128];
// 统计目标字符
for (char c : t.toCharArray()) {
target[c]++;
}
int left = 0, right = 0;
int validCount = 0;
int minLen = Integer.MAX_VALUE;
int start = 0;
int sLen = s.length(), tLen = t.length();
for (; right < sLen; right++) {
char in = s.charAt(right);
window[in]++; // 进窗口
if (window[in] <= target[in]) {
validCount++; // 有效字符
}
// 找到覆盖子串,尝试收缩窗口
while (validCount == tLen) {
// 更新最小覆盖子串
if (right - left + 1 < minLen) {
minLen = right - left + 1;
start = left;
}
char out = s.charAt(left);
if (window[out] <= target[out]) {
validCount--; // 减少有效字符
}
window[out]--; // 出窗口
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}
关键:维护有效字符计数,找到覆盖后立即收缩
滑动窗口总结
适用场景:
- 连续子数组/子字符串问题
- 需要维护某种"条件"的区间
- 时间复杂度要求O(n)
解题步骤:
- 确定窗口维护的"条件"(和、字符种类、有效计数等)
- 右指针扩大窗口,更新条件
- 当条件满足时,更新结果并收缩窗口
- 左指针收缩窗口,恢复条件
复杂度分析:
- 时间复杂度:O(n),每个元素进出窗口各一次
- 空间复杂度:O(字符集大小)
常见变体:
- 固定大小窗口(如异位词问题)
- 可变大小窗口(如最小子数组)
- 多条件窗口(如水果成篮)
掌握这些模板,就能解决大多数滑动窗口问题!