1. 什么是定长滑动窗口算法
滑动窗口算法是解决数组/字符串子区间问题的一类经典方法。定长滑动窗口特指窗口大小固定不变的情况,与之相对的是变长滑动窗口。这种算法通过维护一个固定大小的"窗口"在数据序列上滑动,高效地计算窗口内的统计量或检查某种性质。
我第一次接触这个概念是在解决力扣第567题"字符串的排列"时。题目要求判断s2是否包含s1的排列,这正是定长滑动窗口的典型应用场景。与暴力解法O(n^2)的时间复杂度相比,滑动窗口可以将复杂度降至O(n)。
定长滑动窗口的核心思想是:
- 窗口大小固定为k
- 窗口从序列起始位置开始
- 每次向右滑动一个单位
- 根据新旧元素更新窗口状态
- 在每次滑动后检查窗口是否满足条件
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 定长滑动窗口的三大应用场景
2.1 字符串匹配与排列问题
力扣第567题"字符串的排列"是这类问题的代表。给定两个字符串s1和s2,判断s2是否包含s1的排列。解决思路是:
- 统计s1的字符频率作为目标
- 在s2上维护一个长度为len(s1)的窗口
- 统计窗口内字符频率
- 滑动时去掉最左字符,加入新右字符
- 比较当前窗口频率与目标频率
python复制def checkInclusion(s1: str, s2: str) -> bool:
from collections import defaultdict
target = defaultdict(int)
for c in s1:
target[c] += 1
window = defaultdict(int)
left = 0
for right in range(len(s2)):
window[s2[right]] += 1
if right >= len(s1) - 1:
if window == target:
return True
window[s2[left]] -= 1
if window[s2[left]] == 0:
del window[s2[left]]
left += 1
return False
2.2 子数组/子字符串统计问题
力扣第1343题"大小为K且平均值大于等于阈值的子数组数目"要求统计所有长度为k的子数组中平均值≥threshold的数量。这类问题通常:
- 先计算初始窗口的和
- 滑动时减去离开的元素,加上新进入的元素
- 根据更新后的和判断是否满足条件
python复制def numOfSubarrays(arr: List[int], k: int, threshold: int) -> int:
target = k * threshold
window_sum = sum(arr[:k])
count = 1 if window_sum >= target else 0
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i-k]
if window_sum >= target:
count += 1
return count
2.3 重复元素检测问题
力扣第219题"存在重复元素II"要求在数组中找到两个相同元素,且它们的索引差≤k。使用定长滑动窗口:
- 维护一个最多包含k+1个元素的集合
- 当集合大小超过k+1时移除最早的元素
- 每次添加新元素前检查是否已存在
python复制def containsNearbyDuplicate(nums: List[int], k: int) -> bool:
window = set()
for i, num in enumerate(nums):
if num in window:
return True
window.add(num)
if len(window) > k:
window.remove(nums[i-k])
return False
3. 定长滑动窗口的通用模板
经过多次实践,我总结出一个适用于大多数定长滑动窗口问题的Python模板:
python复制def fixedSlidingWindow(nums, k):
# 初始化窗口状态
window = ... # 可能是计数器、集合、累加器等
result = ... # 存储最终结果
# 处理初始窗口
for i in range(k):
update(window, nums[i])
check(window, result)
# 滑动窗口
for i in range(k, len(nums)):
remove(window, nums[i-k]) # 移除离开窗口的元素
add(window, nums[i]) # 添加新进入窗口的元素
check(window, result) # 检查当前窗口状态
return result
实际应用中,需要根据具体问题实现update、remove、add和check这四个操作。例如在求子数组平均值问题时:
- update/add: 累加元素值
- remove: 减去元素值
- check: 比较平均值与阈值
4. 定长滑动窗口的优化技巧
4.1 提前终止
在某些问题中,一旦找到符合条件的窗口就可以立即返回,不必继续滑动。例如在字符串排列问题中,发现匹配即可返回True:
python复制if window == target:
return True # 提前终止
4.2 状态压缩
对于只关心特定统计量的问题,可以不必维护完整窗口。例如子数组和问题只需维护当前和,不需要存储窗口内所有元素:
python复制window_sum = sum(nums[:k]) # 只需维护和,而非整个子数组
4.3 并行处理
当需要处理多个定长窗口时,可以考虑并行计算。例如同时检查多个k值的情况,可以复用部分计算结果。
5. 常见错误与调试技巧
5.1 窗口大小错误
最容易犯的错误是窗口大小计算不正确。记住:
- 窗口大小通常等于问题给定的k
- 但有时需要k+1(如重复元素检测问题)
- 数组索引从0开始容易导致off-by-one错误
调试方法:打印窗口的左右边界和内容,确保大小符合预期。
5.2 边界条件处理
特别注意以下边界情况:
- 输入数组长度小于k
- k=0或k=1的特殊情况
- 输入包含空数组或空字符串
建议先处理这些边界条件再进入主逻辑。
5.3 状态更新不及时
滑动时务必先移除离开元素,再添加新元素,最后检查条件。顺序错误会导致统计不准确。
典型错误模式:
python复制# 错误顺序!
add(new_element)
remove(old_element)
check()
6. 力扣经典题目实战
6.1 力扣643 - 子数组最大平均数I
题目要求找到长度为k的连续子数组的最大平均数。解法:
python复制def findMaxAverage(nums: List[int], k: int) -> float:
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
关键点:
- 计算初始窗口和
- 滑动时减去nums[i-k],加上nums[i]
- 维护最大和而非直接计算平均数(避免重复除法)
6.2 力扣1052 - 爱生气的书店老板
这是一道稍复杂的定长滑动窗口问题。书店老板在连续X分钟内可以控制情绪,需要最大化满意顾客数。
python复制def maxSatisfied(customers: List[int], grumpy: List[int], X: int) -> int:
base = 0
for i in range(len(customers)):
if not grumpy[i]:
base += customers[i]
max_increase = 0
current = 0
# 初始窗口
for i in range(X):
if grumpy[i]:
current += customers[i]
max_increase = current
# 滑动窗口
for i in range(X, len(customers)):
if grumpy[i-X]:
current -= customers[i-X]
if grumpy[i]:
current += customers[i]
max_increase = max(max_increase, current)
return base + max_increase
解题思路:
- 先计算老板不生气时的基础满意数
- 用滑动窗口找出X分钟内能挽回的最大顾客数
- 两者相加即为结果
6.3 力扣1100 - 长度为K的无重复字符子串
这道题要求找出所有长度为K且无重复字符的子串。解法:
python复制def numKLenSubstrNoRepeats(s: str, k: int) -> int:
if k > len(s):
return 0
count = 0
freq = {}
left = 0
# 初始窗口
for right in range(k):
freq[s[right]] = freq.get(s[right], 0) + 1
if len(freq) == k:
count += 1
# 滑动窗口
for right in range(k, len(s)):
# 移除左边字符
left_char = s[right-k]
freq[left_char] -= 1
if freq[left_char] == 0:
del freq[left_char]
# 添加新字符
new_char = s[right]
freq[new_char] = freq.get(new_char, 0) + 1
# 检查
if len(freq) == k:
count += 1
return count
关键点:
- 使用字典统计字符频率
- 当字典大小等于k时说明无重复
- 注意处理字符频率减至0的情况
7. 滑动窗口与其他算法的比较
7.1 与暴力解法的对比
以字符串排列问题为例:
- 暴力解法:检查s2所有长度为len(s1)的子串,时间复杂度O(n*m)
- 滑动窗口:维护固定窗口,每次滑动只更新两个字符的计数,时间复杂度O(n)
7.2 与动态规划的关系
某些滑动窗口问题可以用DP解决,但滑动窗口通常:
- 空间复杂度更低(O(1) vs O(n))
- 实现更简单
- 但适用场景更特定
7.3 与双指针技术的区别
双指针通常用于:
- 窗口大小不固定的问题
- 需要根据条件动态调整左右指针
- 如力扣第3题"无重复字符的最长子串"
而定长滑动窗口的指针移动是规律且固定的。
8. 滑动窗口的进阶应用
8.1 多维滑动窗口
对于二维矩阵,可以应用滑动窗口思想。例如力扣第1074题"元素和为目标值的子矩阵数量",可以结合行前缀和与列滑动窗口。
8.2 滑动窗口与哈希的结合
很多问题需要结合哈希表来高效统计窗口状态。如前文的字符频率统计,使用字典比数组更灵活。
8.3 时间序列上的滑动窗口
处理时间序列数据时,滑动窗口可用于计算移动平均、检测异常等。例如计算最近1小时的网站访问量。
9. 性能分析与优化
9.1 时间复杂度分析
典型的定长滑动窗口算法:
- 初始化窗口:O(k)
- 滑动过程:O(n-k)
- 总时间复杂度:O(n)
- 空间复杂度:通常O(1)或O(k)
9.2 实际运行效率
在力扣平台上,Python实现的滑动窗口解法通常能在100ms内完成大多数测试用例。影响性能的因素包括:
- 窗口状态更新的复杂度
- 条件检查的成本
- 语言特性(如Python的字典操作比列表稍慢)
9.3 优化建议
- 尽量使用基本数据类型而非复杂对象
- 避免在窗口滑动过程中进行不必要的拷贝
- 对于固定范围的字符(如小写字母),用数组代替字典
- 提前终止不必要的计算
10. 个人实战经验分享
在刷了50+道滑动窗口题目后,我总结出以下经验:
- 画图辅助:在纸上画出窗口滑动过程,有助于理清边界条件
- 模板起步:先套用通用模板,再根据具体问题调整
- 测试用例:特别注意k=0、k=1、k=n等边界情况
- 调试技巧:在循环中打印窗口状态,验证更新逻辑
- 性能监控:对于大数据集,关注内存使用情况
一个常见的陷阱是忘记处理初始窗口。我建议明确分开初始化和滑动两个阶段,就像前文的模板所示。
另一个实用技巧是使用collections.defaultdict来简化频率统计代码,避免繁琐的键存在性检查。但要注意,在比较两个defaultdict时,不存在的键会被认为值为0,这可能在某些场景下导致意外行为。
