1. 滑动窗口算法:从入门到精通的完整指南
滑动窗口算法(Sliding Window Algorithm)是解决数组/字符串子区间问题的高效技巧。我第一次接触这个概念是在解决LeetCode第3题"无重复字符的最长子串"时,当时暴力解法总是超时,直到发现滑动窗口这个"神器"。
1.1 算法核心思想解析
滑动窗口的本质是维护一个动态变化的子区间,通过调整窗口边界来遍历所有可能的解。与暴力枚举相比,它通过避免重复计算将时间复杂度从O(n²)优化到O(n)。举个例子:要在字符串"abcabcbb"中找最长无重复子串,窗口会这样滑动:
code复制[abc]abcbb → [bca]bcbb → [cab]cbb → [abc]bb → [cb]b → [b]
窗口左右边界(left/right指针)像橡皮筋一样伸缩,始终保持窗口内元素的合法性。这种"弹性扫描"的方式正是其高效的关键。
1.2 滑动窗口的两种经典模式
固定窗口大小:窗口宽度恒定,如求长度为k的子数组最大和。模板如下:
python复制left = 0
for right in range(len(arr)):
if right >= k - 1: # 窗口成型后
process_window(arr[left:right+1])
left += 1 # 同步移动左右边界
可变窗口大小:窗口根据条件动态调整,如前面提到的无重复子串问题。典型模板:
python复制left = 0
window = {} # 用于记录窗口内元素
for right in range(len(s)):
window[s[right]] = window.get(s[right], 0) + 1
while 不满足条件: # 如窗口内字符重复
window[s[left]] -= 1
left += 1
update_result() # 更新最优解
关键区别:固定窗口的left/right同步移动,而可变窗口的left可能跳跃前进。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. LeetCode高频考题深度剖析
2.1 无重复字符的最长子串(LeetCode 3)
这是滑动窗口的"Hello World"。解题时需要:
- 用哈希表记录字符最后出现位置
- 当字符重复时,快速跳转left指针
- 实时更新最大长度
python复制def lengthOfLongestSubstring(s):
last_seen = {}
left = max_len = 0
for right, char in enumerate(s):
if char in last_seen:
left = max(left, last_seen[char] + 1)
last_seen[char] = right
max_len = max(max_len, right - left + 1)
return max_len
易错点:直接让left=last_seen[char]会导致回退(如"abba"案例)。必须用max(left, ...)保证指针不倒退。
2.2 最小覆盖子串(LeetCode 76)
这是可变窗口的进阶题型,需要:
- 用两个哈希表分别记录目标字符需求和当前窗口统计
- 维护"满足条件的字符数"计数器
- 在满足条件时收缩窗口找最小解
python复制def minWindow(s, t):
from collections import defaultdict
need = defaultdict(int)
for c in t: need[c] += 1
need_cnt = len(t)
left = 0
res = (0, float('inf'))
for right, c in enumerate(s):
if need[c] > 0:
need_cnt -= 1
need[c] -= 1
while need_cnt == 0: # 满足条件
if right - left < res[1] - res[0]:
res = (left, right)
if need[s[left]] == 0:
need_cnt += 1
need[s[left]] += 1
left += 1
return s[res[0]:res[1]+1] if res[1] != float('inf') else ""
调试技巧:在收缩窗口时,只有当need[s[left]]==0时才增加need_cnt,这是保证不遗漏关键字符的要点。
3. 滑动窗口的六大解题模板
3.1 基础模板(可变窗口)
python复制def sliding_window(s):
left = 0
window = {} # 或使用Counter
result = 0
for right in range(len(s)):
# 1. 移入右边界元素
window[s[right]] = window.get(s[right], 0) + 1
# 2. 检查是否满足收缩条件
while 需要收缩的条件:
# 3. 移出左边界元素
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# 4. 更新结果
result = max(result, right - left + 1)
return result
3.2 固定窗口模板
python复制def fixed_window(arr, k):
window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
3.3 计数型问题模板
适用于字符频率统计问题:
python复制def count_substrings(s, t):
from collections import Counter
target = Counter(t)
window = Counter()
match = 0
res = 0
for right in range(len(s)):
if s[right] in target:
window[s[right]] += 1
if window[s[right]] == target[s[right]]:
match += 1
while match == len(target):
res += 1
if s[left] in target:
if window[s[left]] == target[s[left]]:
match -= 1
window[s[left]] -= 1
left += 1
return res
4. 滑动窗口的五大优化技巧
4.1 哈希表预加载技巧
对于需要频繁查询的问题,预先加载目标模式可以提升效率。例如在最小覆盖子串问题中,提前统计t的字符分布:
python复制need = {}
for c in t:
need[c] = need.get(c, 0) + 1
need_cnt = len(need) # 独特字符数
4.2 指针跳跃优化
当发现重复字符时,left可以直接跳到重复字符的下一位,而非逐步移动:
python复制if char in last_seen:
left = max(left, last_seen[char] + 1)
4.3 频次统计的延迟删除
在统计窗口内元素时,可以保留频次为0的键,避免频繁的字典删除操作:
python复制window[c] -= 1
if window[c] == 0:
del window[c] # 可选操作,不影响逻辑
4.4 结果缓存的时机选择
更新最终结果的时机有讲究:
- 在收缩循环内部更新:确保每次满足条件时都记录
- 在收缩循环之后更新:适用于找最大窗口的情况
4.5 复合条件的处理策略
当需要同时满足多个条件时(如同时满足最小长度和特定条件),可以采用分层判断:
python复制while 满足主要条件:
if 满足次要条件:
更新结果
移动左边界
5. 滑动窗口与其他算法的组合应用
5.1 与前缀和结合
解决子数组和问题时,滑动窗口常需前缀和辅助。例如LeetCode 560题:
python复制def subarraySum(nums, k):
from collections import defaultdict
prefix_sum = defaultdict(int)
prefix_sum[0] = 1
current_sum = 0
count = 0
for num in nums:
current_sum += num
count += prefix_sum.get(current_sum - k, 0)
prefix_sum[current_sum] += 1
return count
5.2 与单调队列结合
解决滑动窗口最大值问题(LeetCode 239)时,需要维护一个单调递减队列:
python复制def maxSlidingWindow(nums, k):
from collections import deque
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
5.3 与双指针的异同
滑动窗口本质是双指针的特例,区别在于:
- 滑动窗口:指针移动与区间内容强相关
- 普通双指针:指针移动可能仅与位置相关(如二分查找)
6. 工业级应用与性能调优
6.1 大数据场景下的窗口优化
处理海量数据流时,可以采用:
- 批处理滑动窗口:按固定批次处理数据
- 分布式窗口:将窗口状态分散到多个节点
- 近似算法:允许一定误差换取吞吐量
6.2 时间窗口与计数窗口
实际系统中常见的变体:
- 时间窗口:如统计最近1分钟的访问量
- 会话窗口:根据业务会话划分窗口
- 全局窗口:处理有限数据流时使用
6.3 容错与状态恢复
生产环境中需要考虑:
- 窗口状态的持久化存储
- 处理迟到数据的策略
- 窗口触发机制的选择(处理时间 vs 事件时间)
7. 滑动窗口的边界条件与测试用例设计
7.1 必须考虑的边界情况
- 空输入处理
- 窗口大于输入长度
- 所有元素相同的情况
- 极值测试(最大/最小整数)
- 重复元素的不同排列组合
7.2 测试用例示例
以LeetCode 3为例的测试矩阵:
| 测试用例 | 预期结果 | 验证要点 |
|---|---|---|
| "" | 0 | 空字符串 |
| "a" | 1 | 单字符 |
| "aaaaa" | 1 | 全重复 |
| "abcabcbb" | 3 | 正常情况 |
| "pwwkew" | 3 | 跳跃重复 |
7.3 调试技巧与日志打印
在开发过程中,可以添加窗口轨迹打印:
python复制print(f"Window [{left}:{right}]: {s[left:right+1]},
Char count: {window}")
这能直观展示窗口移动过程,快速定位逻辑错误。
8. 从滑动窗口到更高级算法
掌握滑动窗口后,可以自然过渡到:
- 动态规划:窗口状态与DP状态转移的相似性
- 贪心算法:局部最优与全局最优的关系
- 图算法:窗口遍历与图遍历的类比
- 流处理框架:Flink/Kafka Streams中的窗口操作
我在实际刷题中发现,很多hard题目都是滑动窗口与其他技巧的组合。例如LeetCode 30题的串联所有单词的子串,就需要结合哈希表和固定窗口技巧。
