滑动窗口:从入门到通解的全场景解析——揭开高效算法的核心密码
引言
算法题里,子数组、子字符串问题特别常见,考得也多。从“长度为k的最大子数组和”到“最小覆盖子串”,从“最长无重复字符子串”到“找字符串里所有字母异位词”,这类问题用传统暴力解法往往得花O(n²)甚至更高的时间。这时候滑动窗口(Sliding Window)就像给算法优化开了扇“动态之门”——通过维护一个能动态调整的窗口,把时间复杂度降到O(n),成了解决这类问题的“效率利器”。
这篇文章咱们从滑动窗口的基础概念讲起,深入聊聊它的核心思路,结合典型场景的代码实现和关键要点,最后提炼出通用解题模板,帮大家从“理解”到“精通”,掌握这个算法设计的核心技巧。
一、滑动窗口的基础概念与核心思想
1.1 什么是滑动窗口?——从生活到算法的类比
滑动窗口这概念,其实能拿现实里的推拉窗打比方:假设有个长度为n的数组(或字符串),窗口就是覆盖其中连续元素的一个“区间”,由左指针(left)和右指针(right)界定。窗口的“滑动”具体怎么体现呢?就是右指针一直往右扩展,左指针根据情况往右收缩,在数组上形成一个动态变化的区间。
比如说在数组[1,3,5,2,4]里,一开始窗口可能是[left=0, right=0](就只包含1);右指针移到2的时候,窗口变成[0,2](包含1,3,5);要是这时候需要收缩左指针到1,窗口就变成[1,2](包含3,5)。整个过程里,窗口就像个能伸缩的“框架”,始终覆盖数组的连续元素。
1.2 中心思想:动态调整的艺术,避免重复计算
滑动窗口的核心思路其实就一句话:通过维护一个能动态变化的连续区间(窗口),靠窗口的扩展和收缩操作,避开暴力解法里的重复计算,把时间复杂度降到线性级。
传统暴力解法里,每个可能的起点(左指针)都得把所有可能的终点(右指针)遍历一遍,这就导致时间复杂度是O(n²)。滑动窗口靠两个关键操作打破了这个限制:
- 右指针单向扩展:右指针从左到右遍历数组,每个元素只被访问一次;
- 左指针按需收缩:左指针根据问题条件(比如窗口里的元素不满足要求)往右移动,每个元素也只被访问一次。
这样一来,整个过程的时间复杂度就是O(n)(每个元素最多被左右指针各访问一次)。就拿计算所有长度为k的子数组和来说,暴力解法得给每个子数组重新求和(O(nk)),滑动窗口则通过“减去左指针移出的元素,加上右指针移入的元素”,把单次求和操作优化成O(1),总时间复杂度降到O(n)。
简单总结一下:滑动窗口的本质就是“用双指针维护动态区间,用状态的增量更新代替重复计算”。
二、滑动窗口的核心操作与基础代码实现
2.1 四步操作法:初始化→扩展右→收缩左→更新结果
不管问题怎么变,滑动窗口的核心操作都能拆成四步(以数组处理为例):
- 初始化:把左指针
left设为0,右指针right也设为0,初始化窗口状态(比如求和变量、用哈希表统计频率这些); - 扩展右边界:右指针往右移动(
right++),把新元素加到窗口里,同时更新窗口状态; - 收缩左边界:根据问题条件(比如窗口里的元素超过限制、不满足目标),循环移动左指针(
left++),直到窗口重新满足条件; - 更新结果:每次调整窗口后,看看当前窗口是不是更优解(比如长度更大、更小之类的),然后记录下来。
这四步就是滑动窗口的“标准流程”,后面所有场景的解法其实都是这个流程的变种。
2.2 固定窗口大小:以“最大子数组和”为例
问题描述:给个数组nums和整数k,找出所有长度为k的连续子数组的最大和。
暴力解法分析:每个起点i(0 ≤ i ≤ n-k)都得算子数组nums[i...i+k-1]的和,时间复杂度O(nk)。
滑动窗口思路:维护一个大小固定为k的窗口,右指针每次右移时,窗口左端的元素被移出,右端的新元素被移入。通过“旧和 - 移出元素 + 移入元素”的方式,把单次求和优化成O(1)。
代码实现(Python):
def max_subarray_sum(nums, k):
n = len(nums)
if n < k:
return 0 # 边界情况:数组长度比k小就没解
current_sum = sum(nums[:k]) # 先算初始窗口的和
max_sum = current_sum
for right in range(k, n):
left_val = nums[right - k] # 移出窗口的左边界元素
current_sum = current_sum - left_val + nums[right] # 增量更新和
if current_sum > max_sum:
max_sum = current_sum
return max_sum
关键点说明:
- 固定窗口的左指针和右指针距离一直是k(
right - left = k),所以左指针位置能直接用right - k算出来,不用单独维护; - 初始窗口的和通过
sum(nums[:k])算,后面用增量更新避免重复求和; - 时间复杂度O(n),空间复杂度O(1)。
2.3 可变窗口大小:以“最长无重复字符子串”为例
问题描述(LeetCode 3):给个字符串s,找出其中不含重复字符的最长子串长度。
暴力解法分析:枚举所有可能的子串,检查有没有重复字符,时间复杂度O(n³)(枚举起点O(n),枚举终点O(n),检查重复O(n))。
滑动窗口思路:维护一个大小可变的窗口,右指针不断扩展,窗口里出现重复字符时,把左指针收缩到重复字符的下一个位置,保证窗口里始终没重复字符。
代码实现(Python):
def length_of_longest_substring(s):
char_index = {} # 哈希表记字符最后出现的索引
max_len = 0
left = 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 # 更新字符最新位置
current_len = right - left + 1 # 当前窗口长度
if current_len > max_len:
max_len = current_len
return max_len
关键点说明:
- 窗口大小是动态变化的(
right - left + 1),左指针只在遇到重复字符时收缩; - 哈希表
char_index用来快速判断当前字符是否在窗口里(比较它最后出现的索引是否≥左指针); - 时间复杂度O(n)(每个字符被右指针访问一次,左指针最多移动n次),空间复杂度O(min(n, m))(m是字符集大小,比如ASCII字符集是128)。
三、滑动窗口的全场景解析:从固定到可变的进阶
滑动窗口的应用场景主要分两种:固定窗口大小和可变窗口大小。前者边界是固定的,后者得根据具体条件动态调整。下面咱们通过具体问题来展开说说。
3.1 固定窗口大小:确定性边界的高效遍历
固定窗口的特点是窗口长度一直是k,左指针和右指针距离固定。这类问题的核心是用“增量更新”代替“重复计算”,常见于求和、求均值这些统计类问题。
典型问题扩展:
- 字符串中的所有字母异位词(LeetCode 438):给字符串
s和p,找出s里所有p的异位词的起始索引。- 思路:维护长度为
len(p)的窗口,统计窗口里字符频率是否和p的频率一样; - 优化:用数组代替哈希表统计频率,通过“匹配计数器”减少每次比较的时间。
- 思路:维护长度为
3.2 可变窗口大小(1):寻找满足条件的最小窗口
问题描述(LeetCode 76):给字符串s和t,找出s里包含t所有字符的最小子串(叫“最小覆盖子串”)。
核心思路:
- 窗口得包含
t的所有字符(每个字符出现次数≥t里的次数); - 右指针扩展直到窗口满足条件,然后左指针尽量收缩,找更小的满足条件的窗口;
- 用两个哈希表分别记
t的字符频率(need)和当前窗口的字符频率(window),用计数器valid记窗口里满足need条件的字符数。
代码实现(Python):
def min_window(s, t):
from collections import defaultdict
need = defaultdict(int)
window = defaultdict(int)
for c in t:
need[c] += 1
left = right = 0
valid = 0 # 记window里满足need条件的字符数
start = 0 # 最小窗口起始位置
min_len = float('inf') # 最小窗口长度
while right < len(s):
c = s[right] # 右指针扩展
right += 1
if c in need:
window[c] += 1
if window[c] == need[c]:
valid += 1 # 这字符数量满足要求
# 窗口满足所有字符需求时,收缩左指针
while valid == len(need):
# 更新最小窗口
if right - left < min_len:
start = left
min_len = right - left
d = s[left] # 左指针收缩
left += 1
if d in need:
if window[d] == need[d]:
valid -= 1 # 这字符数量不再满足要求
window[d] -= 1
return s[start:start+min_len] if min_len != float('inf') else ""
关键点说明:
valid计数器是核心:只有valid == len(need)时,窗口才包含t所有字符;- 左指针收缩时得反向更新
window和valid,保证窗口满足条件时尽量小; - 时间复杂度O(n)(每个字符被左右指针各访问一次),空间复杂度O(m)(m是
t里不同字符的数量)。
3.3 可变窗口大小(2):寻找满足条件的最大窗口
问题描述(LeetCode 1004):给二进制数组nums和整数k,找出最多能翻转k个0的最长连续1子数组长度。
核心思路:
- 窗口里最多允许k个0;
- 右指针扩展时,遇到0就增加
zero_count; zero_count > k时,收缩左指针直到zero_count ≤ k;- 窗口的最大长度就是答案。
代码实现(Python):
def longest_ones(nums, k):
left = 0
zero_count = 0
max_len = 0
for right in range(len(nums)):
if nums[right] == 0:
zero_count += 1
# 0的数量超过k,收缩左指针
while zero_count > k:
if nums[left] == 0:
zero_count -= 1
left += 1
# 更新最大长度
current_len = right - left + 1
if current_len > max_len:
max_len = current_len
return max_len
关键点说明:
- 窗口的“最大”特性靠右指针尽量扩展、左指针只在必要时收缩实现;
- 条件判断的核心是窗口里的“不满足条件项”(比如0的数量)是否超过限制;
- 时间复杂度O(n),空间复杂度O(1)。
3.4 特殊场景:窗口的非连续扩展与多条件处理
有些问题里,窗口调整得同时满足多个条件(比如同时限制最大值和最小值的差、同时包含多种类型元素)。这时候就得更细致地维护窗口状态,比如用有序数据结构(平衡树、堆)记窗口里的元素极值。
典型问题(LeetCode 239):滑动窗口最大值。给数组nums和窗口大小k,返回每个窗口的最大值。
思路:
- 用双端队列(deque)维护窗口里可能的最大值索引;
- 右指针扩展时,从队列尾部移除比当前元素小的索引(它们不可能是后续窗口的最大值);
- 左指针收缩时,要是队列头部索引等于左指针,就从队列头部移除;
- 队列头部一直是当前窗口的最大值索引。
代码实现(Python):
def max_sliding_window(nums, k):
from collections import deque
q = deque() # 存索引,对应元素单调递减
result = []
for right in range(len(nums)):
# 移除队列尾部比当前元素小的索引
while q and nums[right] >= nums[q[-1]]:
q.pop()
q.append(right)
# 移除不在窗口内的索引(左指针是right - k + 1)
while q[0] <= right - k:
q.popleft()
# 窗口形成时(right >= k-1),记最大值
if right >= k - 1:
result.append(nums[q[0]])
return result
关键点说明:
- 双端队列的“单调递减”特性保证了队列头部一直是当前窗口的最大值;
- 每个元素入队和出队各一次,时间复杂度O(n);
- 空间复杂度O(k)(队列最多存k个元素)。
四、滑动窗口的通解模板与适用条件
4.1 通解的核心要素
总结下来,滑动窗口的通用解法得满足这几个关键要素:
- 双指针:左指针(left)和右指针(right)界定窗口的左右边界;
- 状态维护:用哈希表、计数器、队列这些结构记窗口里元素的状态(频率、极值、数量);
- 收缩条件:明确左指针收缩的触发条件(比如窗口里元素不满足要求、超过限制)。
4.2 通用代码模板
下面是滑动窗口的通用伪代码模板,适用于大多数子数组/子字符串问题:
def sliding_window_template(s):
n = len(s)
left = 0
state = ... # 维护窗口状态的结构(哈希表、计数器等)
result = ... # 初始化结果(最小长度、最大和等)
for right in range(n):
# 扩展右指针:更新状态
update_state(right, state)
# 收缩左指针:状态不满足条件时循环收缩
while condition_not_met(state):
update_state(left, state, remove=True) # 反向更新状态
left += 1
# 更新结果
update_result(result, right, left)
return result
模板说明:
update_state函数负责根据右指针(或左指针)的移动,更新窗口状态(比如增加/减少字符频率、更新极值);condition_not_met是收缩左指针的触发条件(像重复字符出现、0的数量超过k);update_result根据具体问题,记录当前窗口的最优解(最大长度、最小和等)。
4.3 适用场景的判断
滑动窗口不是“万能算法”,它的适用场景得满足这几个条件:
- 问题涉及连续子数组/子字符串:滑动窗口依赖窗口的连续性,非连续问题(比如子序列)没法直接用;
- 需要优化时间复杂度:暴力解法时间复杂度高于O(n)(比如O(n²))时,滑动窗口的线性复杂度优势就很明显;
- 存在可维护的“状态”:窗口里的状态得能通过增量更新(频率、和、极值),不然没法高效调整窗口。

1万+

被折叠的 条评论
为什么被折叠?



