本文为我写了首诗,把滑动窗口算法变成了默写题的笔记。
因此,滑动窗口属于双指针的第3种情况。
滑动窗口框架概览
滑动窗口算法技巧主要用来解决子串、子数组问题,比如让你寻找符合某个条件的最长/最短子数组。
滑动窗口算法大致逻辑如下:(C++伪代码)
int left = 0, right = 0;
while (right < nums.size()) {
// 增大窗口
window.add(nums[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(nums[left]);
left++;
}
}
基于滑动窗口算法框架写出的代码,时间复杂度是 O(N)
,比嵌套 for 循环的暴力解法效率高。
下面是滑动窗口算法的代码框架,连在哪里做输出 debug 都给你写好了,以后遇到相关的问题,就默写出来如下框架然后改三个地方就行,保证不会出 bug。
def slidingWindow(s: str):
# 用合适的数据结构记录窗口中的数据,根据具体场景变通
# 比如说,我想记录窗口中元素出现的次数,就用 map
# 我想记录窗口中的元素和,就用 int
window = dict()
# left和right均初始化在最左边
left = 0
right = 0
# right达到s字符串的尽头
while right < len(s):
# c 是将移入窗口的字符
c = s[right]
window[c] = window.get(c, 0) + 1
# 增大窗口
right += 1
# 进行窗口内数据的一系列更新
#...
#/*** debug 输出的位置 ***/
# 注意在最终的解法代码中不要 print
# 因为 IO 操作很耗时,可能导致超时
# print(f"window: [{left}, {right})")
#/********************/
# 判断左侧窗口是否要收缩
while left < right and "window needs shrink":
# d 是将移出窗口的字符
d = s[left]
window[d] -= 1
if window[d] == 0:
del window[d]
# 缩小窗口
left += 1
# 进行窗口内数据的一系列更新
#...
这两个 ...
处的操作分别是扩大和缩小窗口的更新操作,它们操作是完全对称的。
思路版本:
def slidingWindow(s: str):
# 用合适的数据结构记录窗口中的数据,根据具体场景变通map, dict, int
# 将left和right均初始化在最左边
# 判断right是否达到s字符串的尽头:
# c 是将移入窗口的字符
# 增大窗口
# 进行窗口内数据的一系列更新
#/*** debug 输出的位置 ***/
#/********************/
# 判断左侧窗口是否要收缩:
# d 是将移出窗口的字符
# 缩小窗口
# 进行窗口内数据的一系列更新
76. 最小覆盖子串
滑动窗口算法的思路
- 我们在字符串
S
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。
💡为什么要「左闭右开」区间
理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。
因为这样初始化
left = right = 0
时区间[0, 0)
中没有元素,但只要让right
向右移动(扩大)一位,区间[0, 1)
就包含一个元素0
了。如果你设置为两端都开的区间,那么让
right
向右移动一位后开区间(0, 1)
仍然没有元素;如果你设置为两端都闭的区间,那么初始区间[0, 0]
就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
2. 先不断地增加 right
指针扩大窗口 [left, right)
,直到窗口中的字符串符合要求(包含了 T
中的所有字符)。
3. 此时,我们停止增加 right
,转而不断增加 left
指针缩小窗口 [left, right)
,直到窗口中的字符串不再符合要求(不包含 T
中的所有字符了)。同时,每次增加 left
,我们都要更新一轮结果。
4. 重复第 2 和第 3 步,直到 right
到达字符串 S
的尽头。
第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。
左右指针轮流前进,窗口大小增增减减,就好像一条毛毛虫,一伸一缩,不断向右滑动,这就是「滑动窗口」这个名字的来历。——因此,滑动窗口总是初始化为 left = right = 0 的。
Python如何判断字符串条件
对于这点,labuladong的代码没有Python特性。参考【代码随想录】-哈希表242.有效的字母异位词。
Counter不知道是否便于window手动操作,先用defaultdict试一下。——不行,return s_counter > t_counter 是无效的。
由于滑动窗口已经很复杂了,直接用3种方法中最简单的Counter。测试了一下,return s_counter > t_counter 是有效的。而且用Counter也可以对window的单个元素手动操作。
用242尝试了。
from collections import Counter s_counter = Counter(s) t_counter = Counter(t) lst = [] for i in s_counter: if i == "g": s_counter[i] -= 1 return s_counter
结果:
{"a":4,"n":1,"g":0,"r":1,"m":1,"x":1}
套用滑动窗口模板
现在开始套模板,只需要思考以下几个问题:
- 什么时候应该移动
right
扩大窗口?窗口加入字符时,应该更新哪些数据? - 什么时候窗口应该暂停扩大,开始移动
left
缩小窗口?从窗口移出字符时,应该更新哪些数据? - 我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
——如果一个字符进入窗口,应该增加 window
计数器;如果一个字符将移出窗口的时候,应该减少 window
计数器;当 字符串条件 满足 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
我的初始代码:
class Solution:
def minWindow(self, s: str, t: str) -> str:
from collections import Counter
window = Counter()
t_dict = Counter(t)
min_str = s
left = right = 0
while right < len(s):
c = s[right] # 不是right+1,而是right. 因为此时window里面没有元素。
window[c] += 1
right += 1
# debug
print(f"window: [{left}, {right})", window)
while (left < right and window > t_dict):
# 收缩窗口前更新min_str
if right - left < len(min_str):
min_str = s[left:right]
print(f"min_str: {min_str}")
d = s[left]
window[d] -= 1
if window[d] == 0:
del window[d]
left += 1
# 收缩窗口后更新min_str
# print(f"min_str: {min_str}")
return min_str
可以试一下,返回结果是不对的。
? 不想用一个计数器valid判断,只用Counter判断可不可以呢?
应该是不可以的。
while (left < right and window > t_dict): 这不是判断left是否收缩的条件,所以整个代码都不对。
照着labuladong修改了一下:
class Solution:
def minWindow(self, s: str, t: str) -> str:
from collections import Counter
window = Counter()
t_dict = Counter(t)
min_str = s
valid = 0
left = right = 0
while right < len(s):
c = s[right] # 不是right+1,而是right. 因为此时window里面没有元素。
if c in t_dict:
window[c] += 1
if window[c] == t_dict[c]:
valid += 1
right += 1
# debug
print(f"window: [{left}, {right})", window)
# 不需要left < right了吗?——试一下
while (left < right and valid == len(t_dict)):
# 收缩窗口前更新min_str
if right - left < len(min_str):
min_str = s[left:right]
print(f"min_str: {min_str}")
d = s[left]
if d in t_dict:
if window[d] == t_dict[d]:
valid -= 1
window[d] -= 1
left += 1
# print(f"min_str: {min_str}")
return min_str
返回结果:
当 t 中是重复字符的时候就不对了。
提交一下labuladong的代码是可以通过的。
差在哪里?
——结尾不一样:
return 改成 return "" if len(min_str) == len(s) else min_str
这样的话case 2又不一致了。
还是要改成初始化length = float('inf'). min_str用s[start:start+length]表示。
为什么不用start就不行?
判断条件不一样,left是在第二个while的判断条件下面,是会变的,用一个start就是记录下来不变的最小length
完整代码:
class Solution:
def minWindow(self, s: str, t: str) -> str:
from collections import Counter
window = Counter()
t_dict = Counter(t)
start, length = 0, float('inf')
valid = 0
left = right = 0
while right < len(s):
c = s[right] # 不是right+1,而是right. 因为此时window里面没有元素。
if c in t_dict:
window[c] += 1
if window[c] == t_dict[c]:
valid += 1
right += 1
# 不需要left < right了吗?——试一下
while (left < right and valid == len(t_dict)):
# 收缩窗口前更新min_str
if right - left < length:
start = left
length = right - left
d = s[left]
if d in t_dict:
if window[d] == t_dict[d]:
valid -= 1
window[d] -= 1
left += 1
return "" if length == float('inf') else s[start:start+length]
顺便说一下,刷完滑动窗口就少刷labuladong的题,讲的没有代码随想录好,而且选的hard题目,很浪费时间,不适合我目前的阶段。
428. 找到字符串中所有字母异位词
滑动窗口框架+收缩窗口的判断条件用Python哈希表Counter——AC! 和 76解法一样。但76不愧是hard.