1. 滑动窗口算法概述
滑动窗口(Sliding Window)是算法领域中一种经典的优化技术,特别适合处理数组或字符串中的连续子序列问题。我第一次接触这个概念是在解决LeetCode上的"最大无重复字符子串"问题时,当时暴力解法的时间复杂度高达O(n²),而采用滑动窗口后直接优化到O(n)。
这种算法的核心思想是维护一个动态变化的窗口(通常由左右指针表示),通过调整窗口边界来寻找满足条件的解。就像我们通过调整相机取景框来捕捉不同范围的画面一样,滑动窗口让我们能够高效地扫描数据集中所有可能的连续区间。
2. 不定长滑动窗口的特点
2.1 与定长窗口的区别
定长滑动窗口的窗口大小固定不变,比如经典的"大小为K的子数组最大和"问题。而不定长滑动窗口的窗口尺寸会根据条件动态调整,这使得它能解决更复杂的问题场景。
2.2 典型应用场景
- 字符串匹配(最小覆盖子串)
- 无重复字符的最长子串
- 满足特定条件的最短连续子数组
- 包含所有指定字符的最短窗口
3. 基础实现框架
3.1 双指针实现
不定长滑动窗口通常采用双指针(快慢指针)实现,基本框架如下:
python复制def sliding_window(s):
left = 0
window = {} # 用于记录窗口内元素状态
result = 0 # 存储最终结果
for right in range(len(s)):
# 1. 将右指针元素加入窗口
window[s[right]] = window.get(s[right], 0) + 1
# 2. 当窗口不满足条件时,移动左指针
while 窗口需要收缩的条件:
# 更新结果(根据具体问题)
# 移除左指针元素
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# 3. 更新结果(根据具体问题)
result = max(result, right - left + 1)
return result
3.2 关键参数解析
left:窗口左边界,初始为0right:窗口右边界,通过循环逐步右移window:记录当前窗口状态的哈希表result:存储最优解的变量
4. 经典问题实战解析
4.1 无重复字符的最长子串
这是LeetCode第3题,典型的不定长滑动窗口应用。
python复制def lengthOfLongestSubstring(s):
left = 0
max_len = 0
char_index = {} # 记录字符最后出现的位置
for right in range(len(s)):
if s[right] in char_index:
# 确保left不会回退
left = max(left, char_index[s[right]] + 1)
char_index[s[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
关键点:当发现重复字符时,直接将left跳到该字符上次出现位置的下一位,避免无效检查。
4.2 最小覆盖子串
LeetCode第76题,难度较大的滑动窗口应用。
python复制def minWindow(s, t):
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
left = 0
valid = 0 # 满足条件的字符数
min_len = float('inf')
result = ""
window = defaultdict(int)
for right in range(len(s)):
c = s[right]
if c in need:
window[c] += 1
if window[c] == need[c]:
valid += 1
while valid == len(need):
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right+1]
d = s[left]
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
left += 1
return result
5. 性能优化技巧
5.1 哈希表选择
根据字符集大小选择合适的哈希结构:
- ASCII字符(128个):直接用数组
[0]*128更高效 - Unicode字符:使用Python的
defaultdict或dict - 仅字母:可以用
[0]*26,通过ord(c)-ord('a')计算索引
5.2 边界条件处理
- 空字符串输入
- 所有字符都相同的情况
- 没有满足条件的解时返回空串或特定值
5.3 循环不变式
保持循环中这些性质始终成立:
- 窗口[left, right]始终满足问题条件(或正在尝试满足)
- result始终记录当前找到的最优解
- 窗口状态变量(如window字典)准确反映当前窗口内容
6. 常见错误与调试
6.1 指针移动错误
典型错误案例:
python复制# 错误:left直接赋值为right,丢失中间可能解
if s[right] in window:
left = right
正确做法应该是逐步收缩左边界,确保不遗漏任何可能的解。
6.2 状态更新不及时
忘记在移动指针时更新窗口状态:
python复制while 收缩条件:
left += 1
# 忘记更新window字典!
6.3 初始条件设置
未正确处理初始状态可能导致第一个窗口判断错误:
python复制# 应该在进入循环前初始化第一个元素的状态
window = {}
left = 0
for right in range(len(s)):
# 缺少对s[right]的初始处理
7. 复杂度分析
7.1 时间复杂度
虽然代码中有嵌套循环,但每个元素最多被左右指针各访问一次,因此大多数不定长滑动窗口算法的时间复杂度都是O(n)。
7.2 空间复杂度
主要取决于额外使用的哈希表大小:
- 固定字符集(如字母):O(1)
- 可能包含所有字符:O(n)
8. 模式识别
当遇到以下特征时,考虑使用不定长滑动窗口:
- 问题涉及数组/字符串的连续子序列
- 需要寻找满足特定条件的最长/最短区间
- 暴力解法涉及嵌套循环检查所有可能子序列
9. 扩展练习建议
- 先掌握基础模板,理解每个步骤的作用
- 从简单问题开始(如无重复字符的最长子串)
- 逐步挑战更复杂场景(如包含重复元素的最长子串)
- 尝试自己推导窗口收缩条件
- 用白板手动模拟算法执行过程
我在实际刷题中发现,真正掌握滑动窗口的关键不是记忆模板,而是理解窗口扩张和收缩的时机。建议每解决一个问题后,用纸笔画出窗口变化过程,这比单纯AC题目收获更大。