滑动窗口算法是解决字符串和数组子区间问题的利器,其核心在于通过双指针动态维护一个满足特定条件的区间窗口。这种算法之所以高效,是因为它避免了暴力解法中大量的重复计算,将时间复杂度从O(n²)优化到O(n)。
在实际应用中,滑动窗口通常分为两种类型:
关键理解点:滑动窗口本质上是双指针技术的特例,通过维护窗口的合法性来高效解决问题。窗口的"滑动"过程实际上是通过左右指针的交替移动实现的。
LeetCode 904题看似是关于水果收集,实则是经典的"最多包含K个不同元素的最长子数组"问题的特例(K=2)。这种问题转化能力是解决算法题的关键。
在实际编码面试中,面试官可能会先提出水果篮的具体场景,然后要求你抽象出通用解法。因此理解问题背后的数学模型至关重要:
java复制class Solution {
public int totalFruit(int[] fruits) {
int[] basket = new int[fruits.length + 1]; // 使用数组替代哈希表
int kinds = 0, left = 0, maxFruits = 0;
for (int right = 0; right < fruits.length; right++) {
if (basket[fruits[right]]++ == 0) kinds++;
while (kinds > 2) {
if (--basket[fruits[left++]] == 0) kinds--;
}
maxFruits = Math.max(maxFruits, right - left + 1);
}
return maxFruits;
}
}
代码优化点解析:
basket[fruits[right]]++同时完成访问和自增时间复杂度O(n)的保证来自于:
空间复杂度O(n)来自basket数组,但实际可以优化为O(1):
new int[100001]常见错误处理:
字母异位词(Anagram)的判断标准:
这决定了我们必须采用频率统计而非简单排序比较,因为:
java复制class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
int[] pCount = new int[26];
for (char c : p.toCharArray()) pCount[c-'a']++;
int[] window = new int[26];
int valid = 0, left = 0;
for (int right = 0; right < s.length(); right++) {
char inChar = s.charAt(right);
if (++window[inChar-'a'] <= pCount[inChar-'a']) valid++;
if (right >= p.length()-1) {
if (valid == p.length()) result.add(left);
char outChar = s.charAt(left++);
if (window[outChar-'a']-- <= pCount[outChar-'a']) valid--;
}
}
return result;
}
}
关键优化技巧:
测试用例:s="cbaebabacd", p="abc"
当n=10^5时,滑动窗口仅需约100ms,而暴力解法可能超时(>1s)
LeetCode 30题的难点在于:
这要求我们将滑动窗口技术进行扩展:
java复制class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> result = new ArrayList<>();
int wordLen = words[0].length(), totalLen = wordLen * words.length;
if (s.length() < totalLen) return result;
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
for (int i = 0; i < wordLen; i++) {
Map<String, Integer> window = new HashMap<>();
int left = i, matched = 0;
for (int right = i; right <= s.length() - wordLen; right += wordLen) {
String word = s.substring(right, right + wordLen);
if (wordCount.containsKey(word)) {
window.put(word, window.getOrDefault(word, 0) + 1);
if (window.get(word) <= wordCount.get(word)) matched++;
while (window.get(word) > wordCount.get(word)) {
String leftWord = s.substring(left, left + wordLen);
window.put(leftWord, window.get(leftWord) - 1);
if (window.get(leftWord) < wordCount.get(leftWord)) matched--;
left += wordLen;
}
if (matched == words.length) {
result.add(left);
String leftWord = s.substring(left, left + wordLen);
window.put(leftWord, window.get(leftWord) - 1);
matched--;
left += wordLen;
}
} else {
window.clear();
matched = 0;
left = right + wordLen;
}
}
}
return result;
}
}
由于单词长度固定为L,我们需要考虑L种不同的起始偏移:
这样确保不会遗漏任何可能的匹配位置。时间复杂度为O(L * n/L) = O(n)
LeetCode 76题要求找到包含目标所有字符的最短子串,其特点包括:
这需要更精细的窗口控制策略:
java复制class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> need = new HashMap<>();
for (char c : t.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1);
Map<Character, Integer> window = new HashMap<>();
int left = 0, valid = 0, start = 0, minLen = Integer.MAX_VALUE;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(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 + 1 < minLen) {
start = left;
minLen = right - left + 1;
}
char d = s.charAt(left++);
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) valid--;
window.put(d, window.get(d) - 1);
}
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}
通过分析这四道题目,我们可以总结出滑动窗口算法的通用模式:
java复制int left = 0, right = 0;
Map/Array window;
int valid = 0; // 或其他状态指标
java复制while (right < s.length()) {
char c = s.charAt(right);
right++;
// 更新窗口状态
}
java复制while (valid == target) {
// 更新最优解
}
java复制 char d = s.charAt(left);
left++;
// 更新窗口状态
}
数据结构选择依据:
例如:
int[26]HashMap<Character, Integer>HashMap<String, Integer>虽然两者都用于优化子区间问题,但适用场景不同:
典型区分:
识别滑动窗口适用场景:
边界条件处理:
复杂度分析能力:
无限循环:
窗口状态不一致:
特殊用例失败:
掌握滑动窗口算法不仅能帮助解决LeetCode题目,更能培养处理流式数据和实时分析的能力,这对现代软件开发中的诸多场景都有重要意义。