🚀 作者 :“码上有前”
🚀 文章简介 :数据结构与算法
🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬
滑动窗口算法经典题型解析:思路、解法与对比
摘要
本文聚焦 LeetCode 中 4 道滑动窗口经典题目(长度最小的子数组、无重复字符的最长子串、串联所有单词的子串、最小覆盖子串 ),深入剖析滑动窗口解题思路,提供 Python 解法,提炼技巧要点。同时补充其他可行方法,从时间复杂度、空间复杂度、适用场景多维度对比,助力读者透彻理解滑动窗口算法的应用逻辑与优势边界 。
关键词
滑动窗口;LeetCode 算法;时间复杂度;空间复杂度;算法对比
一、引言
滑动窗口算法作为双指针算法的重要分支,在处理数组、字符串的子串(子数组)相关问题时表现卓越,能高效将嵌套循环的高复杂度问题优化为线性复杂度。本文选取 4 道典型题目,详细拆解滑动窗口解法,对比其他方法,明晰算法选型逻辑。
二、题目解析与多解法对比
(一)209. 长度最小的子数组
1. 滑动窗口法(核心解法)
思路
用滑动窗口(双指针 left
、right
)维护子数组,right
扩展窗口,当窗口内元素和 ≥ 目标值时,尝试移动 left
缩小窗口,更新最小长度。
代码(Python)
def minSubArrayLen(target, nums):
left = 0
sum_val = 0
min_len = float('inf')
for right in range(len(nums)):
sum_val += nums[right]
while sum_val >= target:
min_len = min(min_len, right - left + 1)
sum_val -= nums[left]
left += 1
return min_len if min_len != float('inf') else 0
技巧
通过窗口的动态伸缩(right
扩展、left
收缩 ),线性遍历找到满足条件的最小子数组,时间复杂度 O(n)O(n)O(n) 。
复杂度
- 时间:O(n)O(n)O(n) ,
left
、right
各遍历数组一次 。 - 空间:O(1)O(1)O(1) ,仅用常数变量 。
2. 前缀和 + 二分查找法(其他方法)
思路
计算前缀和数组,利用前缀和的单调性,对每个 right
,二分查找满足 prefix[right] - prefix[left] ≥ target
的最小 left
,更新最小长度。
代码(Python)
import bisect
def minSubArrayLen(target, nums):
prefix = [0]
for num in nums:
prefix.append(prefix[-1] + num)
min_len = float('inf')
for right in range(1, len(prefix)):
target_val = prefix[right] - target
left = bisect.bisect_left(prefix, target_val, 0, right)
if prefix[right] - prefix[left] >= target:
min_len = min(min_len, right - left)
return min_len if min_len != float('inf') else 0
复杂度
- 时间:O(nlogn)O(n \log n)O(nlogn) ,前缀和构建 O(n)O(n)O(n) ,二分查找 O(logn)O(\log n)O(logn) 每次,共 nnn 次 。
- 空间:O(n)O(n)O(n) ,存储前缀和数组 。
对比
维度 | 滑动窗口法 | 前缀和 + 二分法 |
---|---|---|
时间复杂度 | O(n)O(n)O(n) | O(nlogn)O(n \log n)O(nlogn) |
空间复杂度 | O(1)O(1)O(1) | O(n)O(n)O(n) |
适用场景 | 追求线性时间,空间敏感 | 数组非负(保证前缀和单调)场景 |
滑动窗口法更高效,无需额外空间存储前缀和;前缀和 + 二分法依赖数组非负性(保证前缀和单调 ),空间开销大。
(二)3. 无重复字符的最长子串
1. 滑动窗口 + 哈希表法(核心解法)
思路
用滑动窗口(left
、right
)维护子串,哈希表记录字符最后出现位置。right
扩展时,若字符重复,更新 left
为 max(left, 重复字符上一位置 + 1)
,同步更新最长子串长度。
代码(Python)
def lengthOfLongestSubstring(s):
char_map = {}
left = 0
max_len = 0
for right in range(len(s)):
if s[right] in char_map:
left = max(left, char_map[s[right]] + 1)
char_map[s[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
技巧
利用哈希表快速判断字符是否重复及定位上一位置,保证窗口内无重复字符,线性遍历求解,时间复杂度 O(n)O(n)O(n) 。
复杂度
- 时间:O(n)O(n)O(n) ,
left
、right
遍历字符串一次 。 - 空间:O(k)O(k)O(k) ,kkk 为字符集大小(如英文字母则 k=26k = 26k=26 ) 。
2. 暴力枚举法(其他方法)
思路
枚举所有子串,检查是否无重复字符,记录最长长度。
代码(Python)
def lengthOfLongestSubstring(s):
max_len = 0
n = len(s)
for i in range(n):
for j in range(i + 1, n + 1):
if len(set(s[i:j])) == j - i:
max_len = max(max_len, j - i)
return max_len
复杂度
- 时间:O(n3)O(n^3)O(n3) ,枚举子串 O(n2)O(n^2)O(n2) ,检查重复 O(n)O(n)O(n) 。
- 空间:O(k)O(k)O(k) ,存储集合判断重复 。
对比
维度 | 滑动窗口 + 哈希表法 | 暴力枚举法 |
---|---|---|
时间复杂度 | O(n)O(n)O(n) | O(n3)O(n^3)O(n3) |
空间复杂度 | O(k)O(k)O(k) | O(k)O(k)O(k) |
适用场景 | 常规解题,追求高效 | 理解子串枚举逻辑场景 |
滑动窗口法高效解决问题,暴力法仅适用于极小数据量,无实际应用价值。
(三)30. 串联所有单词的子串
1. 滑动窗口 + 哈希表法(核心解法)
思路
单词长度固定(设为 word_len
),遍历起始位置 0 ~ word_len - 1
。对每个起始位置,用滑动窗口截取长度为 word_len
的片段,哈希表记录单词出现次数,匹配目标单词组合,更新结果。
代码(Python)
from collections import Counter
def findSubstring(s, words):
if not s or not words:
return []
word_len = len(words[0])
word_count = Counter(words)
total_len = word_len * len(words)
res = []
for start in range(word_len):
left = start
current_count = Counter()
for right in range(start, len(s), word_len):
word = s[right:right + word_len]
if word not in word_count:
left = right + word_len
current_count.clear()
continue
current_count[word] += 1
while current_count[word] > word_count[word]:
left_word = s[left:left + word_len]
current_count[left_word] -= 1
left += word_len
if right - left + word_len == total_len:
res.append(left)
return res
技巧
利用单词长度固定的特性,分起始位置遍历;哈希表精准匹配单词出现次数,窗口滑动步长为 word_len
,保证效率。
复杂度
- 时间:O(wordlen×n)O(word_len \times n)O(wordlen×n) ,nnn 为字符串长度,
word_len
次遍历,每次 O(n/wordlen)O(n / word_len)O(n/wordlen) 。 - 空间:O(m)O(m)O(m) ,mmm 为单词数量(存储哈希表 ) 。
2. 暴力枚举 + 哈希表法(其他方法)
思路
枚举所有可能的子串起始位置,截取对应长度子串拆分单词,用哈希表对比是否匹配目标单词组合。
代码(Python)
from collections import Counter
def findSubstring(s, words):
if not s or not words:
return []
word_len = len(words[0])
total_len = word_len * len(words)
word_count = Counter(words)
res = []
for i in range(len(s) - total_len + 1):
sub_s = s[i:i + total_len]
sub_words = [sub_s[j:j + word_len] for j in range(0, total_len, word_len)]
if Counter(sub_words) == word_count:
res.append(i)
return res
复杂度
- 时间:O((n−m×l)×m)O((n - m \times l) \times m)O((n−m×l)×m) ,nnn 为字符串长度,mmm 为单词数量,lll 为单词长度,截取子串、拆分、对比均有开销。
- 空间:O(m)O(m)O(m) ,存储哈希表 。
对比
维度 | 滑动窗口 + 哈希表法 | 暴力枚举 + 哈希表法 |
---|---|---|
时间复杂度 | O(wordlen×n)O(word_len \times n)O(wordlen×n) | O((n−m×l)×m)O((n - m \times l) \times m)O((n−m×l)×m) |
空间复杂度 | O(m)O(m)O(m) | O(m)O(m)O(m) |
适用场景 | 单词数量多、字符串长场景 | 单词数量少、字符串短场景 |
滑动窗口法通过固定步长和窗口伸缩,大幅优化时间;暴力法在数据量大时极慢,仅适合小数据验证逻辑。
(四)76. 最小覆盖子串
1. 滑动窗口 + 哈希表法(核心解法)
思路
用滑动窗口(left
、right
)维护子串,哈希表记录目标字符需求。right
扩展包含目标字符,left
收缩优化窗口,当窗口满足包含所有目标字符时,更新最小覆盖子串。
代码(Python)
from collections import Counter
def minWindow(s, t):
need = Counter(t)
window = Counter()
left = 0
valid = 0
start, min_len = 0, float('inf')
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:
start = left
min_len = right - left + 1
d = s[left]
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
left += 1
return "" if min_len == float('inf') else s[start:start + min_len]
技巧
借助哈希表精准控制窗口内目标字符的覆盖情况,通过 valid
变量判断是否满足条件,动态更新最小子串,时间复杂度 O(n)O(n)O(n) 。
复杂度
- 时间:O(n)O(n)O(n) ,
left
、right
遍历字符串一次 。 - 空间:O(k)O(k)O(k) ,kkk 为目标字符集大小 。
2. 暴力枚举法(其他方法)
思路
枚举所有子串,检查是否包含目标所有字符,记录最小长度子串。
代码(Python)
from collections import Counter
def minWindow(s, t):
min_len = float('inf')
result = ""
t_count = Counter(t)
for i in range(len(s)):
for j in range(i + 1, len(s) + 1):
sub_s = s[i:j]
sub_count = Counter(sub_s)
if all(sub_count[c] >= t_count[c] for c in t_count):
if len(sub_s) < min_len:
min_len = len(sub_s)
result = sub_s
return result
复杂度
- 时间:O(n3)O(n^3)O(n3) ,枚举子串 O(n2)O(n^2)O(n2) ,检查字符 O(n)O(n)O(n) 。
- 空间:O(k)O(k)O(k) ,存储哈希表 。
对比
维度 | 滑动窗口 + 哈希表法 | 暴力枚举法 |
---|---|---|
时间复杂度 | O(n)O(n)O(n) | O(n3)O(n^3)O(n3) |
空间复杂度 | O(k)O(k)O(k) | O(k)O(k)O(k) |
适用场景 | 实际解题、数据量大场景 | 理解子串覆盖逻辑场景 |
滑动窗口法高效解决问题,暴力法仅用于理论理解,无法处理中等及以上数据量。
三、总结
滑动窗口算法通过动态调整窗口边界(双指针伸缩 ),将子串(子数组 )问题的高复杂度优化为线性或近似线性。对比其他方法(前缀和、暴力枚举等 ),在适配场景下(如处理需动态维护区间的问题 ),展现出时间与空间的双重优势。不同题目需结合数据特点(有序性、字符重复性等 )与复杂度要求选型,掌握滑动窗口算法的核心逻辑与对比要点,能显著提升算法解题的效率与精准度,从容应对数组、字符串的区间类问题挑战 。