第一次接触滑动窗口算法是在解决LeetCode第3题"无重复字符的最长子串"时。当时我用了暴力解法,时间复杂度高达O(n²),提交后直接超时。后来看到讨论区有人提到"滑动窗口"这个神奇的概念,只用O(n)时间就解决了问题,从此打开了新世界的大门。
滑动窗口本质上是一种双指针技巧的变体,特别适合解决数组/字符串的子区间问题。它通过维护一个动态变化的窗口来避免重复计算,把很多看似需要O(n²)的问题优化到O(n)时间复杂度。在实际面试中,滑动窗口类题目出现的频率极高,是算法准备中必须掌握的利器。
滑动窗口算法主要用于解决数组或字符串的连续子区间问题。典型场景包括:
这类问题的共同特点是都需要考察输入序列的某个连续区间,而滑动窗口可以高效地枚举所有可能的区间,同时避免重复计算。
经过几十道题的实战,我总结出了一个通用的滑动窗口模板:
python复制def slidingWindow(s: str, t: str) -> str:
# 初始化哈希表和计数器
need = collections.defaultdict(int)
for c in t:
need[c] += 1
count = len(t)
left = right = 0 # 窗口左右边界
min_len = float('inf') # 记录最小窗口长度
result = ""
while right < len(s):
# 右边界扩展
if s[right] in need:
if need[s[right]] > 0:
count -= 1
need[s[right]] -= 1
# 满足条件时收缩左边界
while count == 0:
# 更新结果
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right+1]
# 左边界移动
if s[left] in need:
need[s[left]] += 1
if need[s[left]] > 0:
count += 1
left += 1
right += 1
return result
这个模板适用于大多数滑动窗口问题,只需根据具体题目调整条件判断和结果更新部分。
这是滑动窗口最经典的入门题。给定一个字符串,找出不含有重复字符的最长子串的长度。
python复制def lengthOfLongestSubstring(s: str) -> int:
char_index = {} # 记录字符最后出现的位置
left = max_len = 0
for right, char in enumerate(s):
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1
char_index[char] = right
max_len = max(max_len, right - left + 1)
return max_len
关键点:
时间复杂度O(n),空间复杂度O(min(m,n)),其中m是字符集大小。
给定字符串S和T,在S中找到包含T所有字符的最短子串。
python复制def minWindow(s: str, t: str) -> str:
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
count = len(t)
left = 0
min_len = float('inf')
result = ""
for right, char in enumerate(s):
if char in need:
if need[char] > 0:
count -= 1
need[char] -= 1
while count == 0:
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right+1]
left_char = s[left]
if left_char in need:
need[left_char] += 1
if need[left_char] > 0:
count += 1
left += 1
return result
这个实现完美展示了滑动窗口的典型结构:
判断s2是否包含s1的排列,即是否存在s2的子串是s1的某种排列。
python复制def checkInclusion(s1: str, s2: str) -> bool:
from collections import defaultdict
need = defaultdict(int)
for c in s1:
need[c] += 1
count = len(s1)
left = 0
for right, char in enumerate(s2):
if char in need:
if need[char] > 0:
count -= 1
need[char] -= 1
while right - left + 1 > len(s1):
left_char = s2[left]
if left_char in need:
need[left_char] += 1
if need[left_char] > 0:
count += 1
left += 1
if count == 0 and right - left + 1 == len(s1):
return True
return False
这道题的技巧在于窗口大小固定为s1的长度,只需检查是否存在某个窗口的字符计数与s1完全匹配。
滑动窗口问题可以分为两大类:
固定窗口问题通常更简单,因为不需要维护窗口的最大/最小条件,只需滑动窗口并检查每个位置即可。
有些复杂问题可能需要维护多个指针。例如LeetCode 424"替换后的最长重复字符",需要同时维护窗口的左边界和当前最大计数字符的位置。
python复制def characterReplacement(s: str, k: int) -> int:
count = {}
max_count = left = max_len = 0
for right, char in enumerate(s):
count[char] = count.get(char, 0) + 1
max_count = max(max_count, count[char])
if (right - left + 1 - max_count) > k:
count[s[left]] -= 1
left += 1
max_len = max(max_len, right - left + 1)
return max_len
这个实现巧妙地使用max_count来跟踪窗口内最多出现的字符数,避免每次重新计算。
滑动窗口常与其他算法结合使用:
例如LeetCode 239"滑动窗口最大值"就使用了单调队列来优化:
python复制def maxSlidingWindow(nums: List[int], k: int) -> List[int]:
from collections import deque
q = deque()
result = []
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:
result.append(nums[q[0]])
return result
滑动窗口算法最容易出错的就是边界条件:
建议在纸上画出窗口移动过程,特别是处理以下情况:
使用哈希表记录字符出现次数时,常见错误包括:
调试时可以打印窗口状态和哈希表内容:
python复制print(f"Window: [{left}, {right}], Count: {count}, Hash: {need}")
当处理大规模数据时,可以考虑:
例如对于仅包含小写字母的问题,可以用长度为26的数组代替哈希表:
python复制count = [0] * 26
for c in t:
count[ord(c) - ord('a')] += 1
为了系统掌握滑动窗口,建议按以下类别刷题:
在技术面试中遇到滑动窗口问题时,建议采取以下步骤:
面试官常考察的点包括:
练习时建议先用伪代码写出框架,再填充具体实现细节,这样可以避免陷入编码细节而忽略整体逻辑。