作为一名刷过300+道LeetCode的老手,我深刻体会到滑动窗口是面试中最常考的核心算法之一。相比暴力解法,它能将很多字符串和数组问题的复杂度从O(n²)优化到O(n)。今天我们就来深入探讨这个专题的第二部分,重点解决那些需要特殊技巧的滑动窗口变种问题。
记得去年面试某大厂时,面试官连续出了三道滑动窗口变种题,幸亏平时对这个专题有系统性的总结。本文将分享我在实战中总结的滑动窗口模板和六大高频变体解法,包含多个力扣原题的精讲(76、340、424、992等)。无论你是刚开始刷题的新手,还是想巩固技巧的老手,这些内容都能帮你建立完整的解题框架。
滑动窗口本质上是通过维护一个动态变化的窗口来减少不必要的计算。它特别适合解决"连续子数组/子字符串"类问题,基本框架包含三个关键要素:
python复制def sliding_window(s):
left = 0
window = {} # 记录窗口状态
for right in range(len(s)):
# 更新窗口状态(扩大窗口)
window[s[right]] = window.get(s[right], 0) + 1
# 判断是否需要收缩窗口
while window需要收缩的条件:
# 更新窗口状态(收缩窗口)
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# 处理当前窗口数据
标准的滑动窗口算法通常能达到O(n)时间复杂度和O(k)空间复杂度(k为字符集大小)。这是因为每个元素最多被左右指针各访问一次,没有嵌套循环带来的平方复杂度。
关键理解:滑动窗口的高效性来自于它避免了暴力解法中的重复计算。比如在字符串查找问题中,它不会重复检查已经确定不符合条件的子串。
这类问题的窗口大小固定为k,典型例题包括:
解法模板:
python复制def maxAverage(nums, k):
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i-k] # 滑动窗口
max_sum = max(max_sum, window_sum)
return max_sum / k
实战技巧:固定窗口问题通常可以用"当前和 = 前一个和 + 新元素 - 移出元素"的公式来优化计算,避免每次重新求和。
这类问题需要在窗口滑动过程中维护最大值,典型例题:
进阶解法使用单调队列:
python复制from collections import deque
def maxSlidingWindow(nums, k):
q = deque()
res = []
for i, num in enumerate(nums):
while q and nums[q[-1]] <= num:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
res.append(nums[q[0]])
return res
避坑指南:
这类问题要求处理包含最多K个不同字符的子串,典型例题:
解法框架:
python复制def lengthOfLongestSubstringKDistinct(s, k):
count = {}
left = 0
max_len = 0
for right in range(len(s)):
count[s[right]] = count.get(s[right], 0) + 1
while len(count) > k:
count[s[left]] -= 1
if count[s[left]] == 0:
del count[s[left]]
left += 1
max_len = max(max_len, right - left + 1)
return max_len
优化技巧:使用OrderedDict可以进一步优化到O(1)时间删除最老的字符,但普通字典在大多数情况下已经足够高效。
这类问题允许进行有限次数的替换操作,典型例题:
核心解法:
python复制def characterReplacement(s, k):
count = {}
max_count = 0
left = 0
for right in range(len(s)):
count[s[right]] = count.get(s[right], 0) + 1
max_count = max(max_count, count[s[right]])
if (right - left + 1) - max_count > k:
count[s[left]] -= 1
left += 1
return len(s) - left
关键理解:窗口长度 - 最大重复字符数 > k时,说明需要替换的字符超过了允许次数,必须收缩窗口。
这是滑动窗口最经典的难题之一,典型例题:
优化解法:
python复制from collections import defaultdict
def minWindow(s, t):
need = defaultdict(int)
for c in t:
need[c] += 1
missing = len(t)
left = 0
min_len = float('inf')
min_left = 0
for right, c in enumerate(s):
if need[c] > 0:
missing -= 1
need[c] -= 1
if missing == 0:
while left < right and need[s[left]] < 0:
need[s[left]] += 1
left += 1
if right - left + 1 < min_len:
min_len = right - left + 1
min_left = left
return s[min_left:min_left+min_len] if min_len != float('inf') else ""
调试心得:
这类问题需要检测排列或异位词,典型例题:
高效解法:
python复制from collections import defaultdict
def findAnagrams(s, p):
need = defaultdict(int)
for c in p:
need[c] += 1
missing = len(p)
res = []
for right, c in enumerate(s):
if right >= len(p):
left_char = s[right - len(p)]
if left_char in need:
if need[left_char] >= 0:
missing += 1
need[left_char] += 1
if c in need:
if need[c] > 0:
missing -= 1
need[c] -= 1
if missing == 0:
res.append(right - len(p) + 1)
return res
常见错误:
在需要统计字符频率的问题中,哈希表是最常用的数据结构。但有几个优化点值得注意:
滑动窗口问题最容易出错的就是边界条件:
当滑动窗口解法出现问题时,可以采用以下调试技巧:
这是滑动窗口最经典的难题,我们来深入分析:
python复制def minWindow(s, t):
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
missing = len(t)
left = 0
min_len = float('inf')
min_left = 0
for right, c in enumerate(s):
if need[c] > 0:
missing -= 1
need[c] -= 1
if missing == 0:
while left < right and need[s[left]] < 0:
need[s[left]] += 1
left += 1
if right - left + 1 < min_len:
min_len = right - left + 1
min_left = left
need[s[left]] += 1
missing += 1
left += 1
return s[min_left:min_left+min_len] if min_len != float('inf') else ""
面试考点:
另一道高频面试题:
python复制def lengthOfLongestSubstringKDistinct(s, k):
from collections import defaultdict
count = defaultdict(int)
left = 0
max_len = 0
for right, c in enumerate(s):
count[c] += 1
while len(count) > k:
count[s[left]] -= 1
if count[s[left]] == 0:
del count[s[left]]
left += 1
max_len = max(max_len, right - left + 1)
return max_len
变体思考:
考察窗口维护技巧:
python复制def characterReplacement(s, k):
count = {}
max_count = 0
left = 0
max_length = 0
for right in range(len(s)):
count[s[right]] = count.get(s[right], 0) + 1
max_count = max(max_count, count[s[right]])
if (right - left + 1) - max_count > k:
count[s[left]] -= 1
left += 1
max_length = max(max_length, right - left + 1)
return max_length
优化洞察:
根据我刷题300+的经验,总结出以下通用解题步骤:
问题分析:
初始化阶段:
窗口滑动阶段:
结果处理:
通用模板:
python复制def sliding_window_template(s):
# 初始化
left = 0
window = {} # 或其他数据结构
result = 0 # 或其他初始值
for right in range(len(s)):
# 更新窗口状态(扩大窗口)
window = update_window(window, s[right])
# 检查收缩条件
while needs_shrink(window):
# 更新窗口状态(收缩窗口)
window = shrink_window(window, s[left])
left += 1
# 更新结果
result = update_result(result, window, left, right)
return result
某些问题需要结合滑动窗口和DP思想,如:
示例解法:
python复制def maxSubArray(nums):
current_max = global_max = nums[0]
for num in nums[1:]:
current_max = max(num, current_max + num)
global_max = max(global_max, current_max)
return global_max
关联思考:这里的current_max实际上维护了一个动态变化的"窗口和"。
对于某些特殊问题,可以结合二分查找:
示例解法:
python复制def minSubArrayLen(target, nums):
left = 0
current_sum = 0
min_length = float('inf')
for right in range(len(nums)):
current_sum += nums[right]
while current_sum >= target:
min_length = min(min_length, right - left + 1)
current_sum -= nums[left]
left += 1
return min_length if min_length != float('inf') else 0
进阶思考:如何用前缀和+二分查找优化这个问题?
当需要高效获取窗口极值时:
python复制from heapq import heappush, heappop
def medianSlidingWindow(nums, k):
small = [] # 最大堆(存储负数实现)
large = [] # 最小堆
result = []
for i, num in enumerate(nums):
# 添加元素到合适的堆
if not small or num <= -small[0][0]:
heappush(small, (-num, i))
else:
heappush(large, (num, i))
# 平衡堆大小
balance_heaps(small, large)
# 移除超出窗口的元素
while small and small[0][1] <= i - k:
heappop(small)
while large and large[0][1] <= i - k:
heappop(large)
# 重新平衡
balance_heaps(small, large)
# 计算中位数
if i >= k - 1:
if k % 2 == 1:
result.append(-small[0][0])
else:
result.append((-small[0][0] + large[0][0]) / 2)
return result
实现细节:需要注意堆元素的延迟删除和索引跟踪。
无限循环:收缩条件设置不当导致left超过right
错误计数:在收缩窗口时未正确更新状态
边界遗漏:未处理空输入或k=0等特殊情况
过早优化:试图一步到位而忽略基础解法
根据我的刷题经验,建议按以下顺序系统练习:
基础窗口滑动:
哈希表辅助计数:
替换/修改类问题:
覆盖/包含类问题:
极值维护问题:
综合难题:
虽然我们主要在算法题中练习滑动窗口,但它在实际工程中也有广泛应用:
以网络流量控制为例,滑动窗口算法帮助实现了:
在刷题时理解这些实际应用场景,能帮助我们更好地掌握算法本质。