抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

前言

滑动窗口是解决数组或字符串中连续子序列问题的一种高效技术。其核心思想是使用两个指针(通常称为左指针和右指针)维护一个”窗口”,根据特定条件动态调整窗口的大小,从而在线性时间复杂度内找到满足条件的解。

适用场景

滑动窗口算法适用于以下场景:

  • 在数组/字符串中寻找最长/最短的满足某条件的连续子数组/子串
  • 在数组/字符串中寻找所有满足某条件的连续子数组/子串
  • 需要计算满足特定条件的连续子序列的数量
  • 检查是否存在满足条件的连续子序列

滑动窗口模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def sliding_window(nums, k):  # k可能是固定窗口大小或其他条件
# 初始化窗口边界和结果
left = 0
result = 0 # 根据题目调整(可能是最大值、最小值、计数等)
window = {} # 用于记录窗口内元素状态的数据结构

# 右指针遍历整个数组/字符串
for right in range(len(nums)):
# 1. 扩大窗口,将右指针元素加入窗口
window[nums[right]] = window.get(nums[right], 0) + 1

# 2. 根据条件收缩窗口(while循环)
while 窗口不满足条件:
# 移除左指针元素
window[nums[left]] -= 1
if window[nums[left]] == 0:
del window[nums[left]]
left += 1

# 3. 更新结果
result = max(result, right - left + 1) # 或其他更新逻辑

return result

滑动窗口的两种类型

1. 固定大小窗口

当窗口大小固定时,模板略有简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fixed_window(nums, k):
# 初始化
left = 0
window = {} # 或其他数据结构
result = 0 # 或其他初始值

for right in range(len(nums)):
# 加入窗口
window[nums[right]] = window.get(nums[right], 0) + 1

# 当窗口大小超过k时,移除左侧元素
if right - left + 1 > k:
window[nums[left]] -= 1
if window[nums[left]] == 0:
del window[nums[left]]
left += 1

# 当窗口大小等于k时,更新结果
if right - left + 1 == k:
# 更新result
pass

return result

2. 可变大小窗口

可变大小窗口更为常见,通常用于寻找最长/最短的满足条件的子数组/子串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def variable_window(nums, condition):
# 初始化
left = 0
window = {} # 或其他数据结构
result = 0 # 或其他初始值

for right in range(len(nums)):
# 加入窗口
window[nums[right]] = window.get(nums[right], 0) + 1

# 根据条件收缩窗口
while 不满足条件:
window[nums[left]] -= 1
if window[nums[left]] == 0:
del window[nums[left]]
left += 1

# 更新结果
result = max/min(result, right - left + 1) # 或其他更新逻辑

return result

经典滑动窗口问题解析

例1: 最长无重复字符的子串 (LeetCode 3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def lengthOfLongestSubstring(s: str) -> int:
char_set = set() # 用集合记录窗口中的字符
left = 0
max_length = 0

for right in range(len(s)):
# 如果出现重复字符,收缩窗口
while s[right] in char_set:
char_set.remove(s[left])
left += 1

# 加入新字符
char_set.add(s[right])

# 更新最大长度
max_length = max(max_length, right - left + 1)

return max_length

例2: 和为k的最长子数组 (LeetCode 560)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def maxSubArrayLen(nums: List[int], k: int) -> int:
prefix_sum = {0: -1} # 前缀和 -> 索引
max_len = 0
curr_sum = 0

for i, num in enumerate(nums):
curr_sum += num

# 如果curr_sum - k存在于prefix_sum中,则找到了一个和为k的子数组
if curr_sum - k in prefix_sum:
max_len = max(max_len, i - prefix_sum[curr_sum - k])

# 只保存第一次出现的前缀和,以获得最长子数组
if curr_sum not in prefix_sum:
prefix_sum[curr_sum] = i

return max_len

例3: 最小覆盖子串 (LeetCode 76)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def minWindow(s: str, t: str) -> str:
if not t or not s:
return ""

# 统计t中每个字符的出现次数
dict_t = {}
for char in t:
dict_t[char] = dict_t.get(char, 0) + 1

# 所需字符数量
required = len(dict_t)

# 窗口初始化
left, right = 0, 0
formed = 0 # 已满足条件的字符种类数
window_counts = {}

# 结果初始化
min_len = float("inf")
result = ""

while right < len(s):
# 扩大窗口
char = s[right]
window_counts[char] = window_counts.get(char, 0) + 1

# 检查字符是否满足条件
if char in dict_t and window_counts[char] == dict_t[char]:
formed += 1

# 尝试收缩窗口
while left <= right and formed == required:
char = s[left]

# 更新结果
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right + 1]

# 收缩窗口
window_counts[char] -= 1
if char in dict_t and window_counts[char] < dict_t[char]:
formed -= 1

left += 1

right += 1

return result

滑动窗口解题技巧

  1. 问题识别: 关键词如”连续子数组”、”子串”、”最长/最短满足条件的子序列”通常暗示可以使用滑动窗口。

  2. 窗口表示: 根据问题选择合适的数据结构表示窗口:

    • 哈希表/字典: 记录元素频率
    • 集合: 记录不重复元素
    • 变量: 记录窗口内的特定状态(如和、乘积等)
  3. 窗口扩张与收缩条件: 明确定义何时扩大窗口,何时收缩窗口。

  4. 结果更新时机: 确定在窗口扩张后还是收缩后更新结果。

  5. 边界条件处理: 注意空数组、空字符串等边界情况。

常见问题变形

  1. 找到所有满足条件的子数组/子串: 在每次窗口满足条件时都记录结果。

  2. 固定大小窗口: 窗口大小恒定为k,简化模板。

  3. 最小/最大窗口: 找出满足条件的最小/最大连续子序列。

  4. 计数问题: 统计满足条件的子数组/子串数量。

时间复杂度分析

滑动窗口算法的时间复杂度通常为 $O(n)$,其中 $n$ 是数组/字符串的长度。这是因为:

  • 右指针最多移动 $n$ 次
  • 左指针最多移动 $n$ 次
  • 窗口内操作通常是 $O(1)$ 或 $O(k)$,其中 $k$ 远小于 $n$

这使得滑动窗口算法在处理连续子序列问题时非常高效。

评论