导语
前面的笔记基本将Leetcode上主要的内容进行了一刷,现在进行回顾总结,本文主要介绍几个滑动窗口相关的题目,并进行总结整理。
所涉及的题目如下:
| 题目列表 | 难度 |
|---|---|
| 209. 长度最小的子数组 | 中等 |
| 487. 最大连续1的个数 II | 中等 |
| 1004. 最大连续1的个数 III | 中等 |
| 340. 至多包含 K 个不同字符的最长子串 | 中等 |
| 424. 替换后的最长重复字符 | 中等 |
| 3. 无重复字符的最长子串 | 中等 |
| 438. 找到字符串中所有字母异位词 | 中等 |
| 30. 串联所有单词的子串 | 困难 |
| 76. 最小覆盖子串 | 困难 |
滑动窗口
滑动窗口是一种常用的算法设计技巧,尤其在处理数组或字符串等序列型数据结构时特别有用。滑动窗口通常用于解决子数组或子序列问题,例如找出给定数组中和为特定值的连续子数组,或找出数组中的最长连续子数组等。
滑动窗口的基本思想
假设有一个数组或字符串,你可以想象有一个窗口在这个数组或字符串上从左至右滑动。窗口的大小可以是固定的,也可以是可变的。在窗口滑动的过程中,你可以进行各种计算,以解决特定问题。
一般步骤
- 定义窗口的左右边界,通常用两个指针表示。
- 根据问题需求初始化一些其他变量,例如
maxSum、minLen等。 - 移动右边界来扩大窗口,并更新相关变量。
- 检查是否需要缩小窗口(即移动左边界)。缩小窗口通常发生在窗口内的数据已经满足了某个条件(例如和超过了给定值)。
- 在滑动的过程中,根据问题需求,进行必要的计算或记录。
模板
参考labuladong.github.io/algo/di-lin… ,实际上滑动窗口作为一类特殊的双指针,解题套路相对固定。
def slidingWindow(s: str):
# 用合适的数据结构记录窗口中的数据,如set或者dict之类的
counter = {}
left, right = 0, 0
val = …… # 要求的最大或者最小长度、序列之类的
while right < len(s):
# c 是将移入窗口的字符、元素
c = s[right]
counter[c] += 1
# 增大窗口
right += 1
# 进行窗口内数据的一系列更新
# ...
# 判断左侧窗口是否要收缩,这里可以为一行代码逻辑,也可以单独写一个判别函数
while left < right and window needs shrink:
# d 是将移出窗口的字符、元素
d = s[left]
# 缩小窗口
left += 1
# 进行窗口内数据的一系列更新
# ...
209. 长度最小的子数组
题目描述可以参考我之前的博客Leetcode刷题笔记2:数组基础2,这里不再赘述。这里我们可以套用这个模板,最外层循环也可以使用for循环,这样就不需要手动更新right。解题代码如下:
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# 初始化 start 指针为 0,用于标识子数组的起始位置
start = 0
# 初始化 ans 为 0,用于保存子数组的和
ans = 0
# 初始化 min_length 为数组长度+1,用于保存满足条件的最短子数组长度
min_length = len(nums) + 1
# 使用 end 指针遍历整个数组
for end in range(len(nums)):
# 计算从 start 到 end 的子数组的和
ans += nums[end]
# 如果子数组的和大于等于目标值
while ans >= target:
# 更新 min_length
min_length = min(end - start + 1, min_length)
# 从子数组的和中减去 start 指针指向的元素
ans -= nums[start]
# 将 start 指针向右移动一位,以缩小子数组的大小
start += 1
# 如果 min_length 没有被更新(即仍然是初始值),说明没有找到满足条件的子数组,返回 0
if min_length == len(nums) + 1:
return 0
# 否则,返回找到的最短子数组的长度
else:
return min_length
这里的end就是模板中的right,只需要O(N)的复杂度我们就可以解答。
487. 最大连续1的个数 II
题目描述
给定一个二进制数组
nums,如果最多可以翻转一个0,则返回数组中连续1的最大个数。 提示:1 <= nums.length <= 105;nums[i]不是0就是1.进阶: 如果输入的数字是作为 无限流 逐个输入如何处理?换句话说,内存不能存储下所有从流中输入的数字。您可以有效地解决吗?
解答
直接套用模板,我们这里维护的窗口中只能最多反转1个零,所以当flip_count大于1时,需要移动left,当遇到0时我们再将flip_count恢复就可以了。
from typing import List
class Solution:
def findMaxConsecutiveOnes(self, nums: List[int]) -> int:
left, right = 0, 0 # 定义滑动窗口的左右边界
max_count = 0 # 初始化最大连续1的个数为0
flip_count = 0 # 初始化翻转0的计数为0
while right < len(nums): # 通过移动右边界来遍历数组
if nums[right] == 0: # 如果当前元素是0,则增加翻转计数
flip_count += 1
# 如果翻转计数大于1(即窗口内有多于一个的0)
# 则需要缩小窗口(通过移动左边界)
while flip_count > 1:
if nums[left] == 0: # 如果左边界元素是0,则减少翻转计数
flip_count -= 1
left += 1 # 移动左边界来缩小窗口
# 更新最大连续1的个数
max_count = max(max_count, right - left + 1)
right += 1 # 移动右边界来扩大窗口
return max_count # 返回最大连续1的个数
1004. 最大连续1的个数 III
这道题目将1变为了k,实际上只是滑动窗口中的含义变化了,也就是窗口中最多只能反转k个零,当flip_count大于k时,需要移动left并将flip_count-=1.
[left, right] 中永远是一个合法的区间。
class Solution:
def longestOnes(self, nums: List[int], k: int) -> int:
left, right = 0, 0 # 初始化滑动窗口的左右边界
max_count = 0 # 初始化最大连续1的个数为0
flip_count = 0 # 初始化翻转的0的数量为0
# 遍历数组(通过移动右边界来扩大窗口)
while right < len(nums):
# 如果当前元素是0,则增加翻转计数
if nums[right] == 0:
flip_count += 1
# 如果翻转计数大于k(即窗口内的0数量超过了允许翻转的数量)
# 需要缩小窗口(通过移动左边界)
while flip_count > k:
# 如果左边界处的元素是0,则减少翻转计数
if nums[left] == 0:
flip_count -= 1
left += 1 # 移动左边界来缩小窗口
# 更新最大连续1的个数
max_count = max(max_count, right - left + 1)
right += 1 # 移动右边界来扩大窗口
return max_count # 返回最大连续1的个数
340. 至多包含 K 个不同字符的最长子串
题目描述
给你一个字符串
s和一个整数k,请你找出 至多 包含k个 不同 字符的最长子串,并返回该子串的长度。示例 1:
输入: s = "eceba", k = 2
输出: 3
解释: 满足题目要求的子串是 "ece" ,长度为 3 。
提示:
1 <= s.length <= 5 * 1040 <= k <= 50
解法
class Solution:
def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
if k == 0 or len(s) == 0:
return 0
left = 0
max_length = 0
char_frequency = defaultdict(int) # 哈希表用于记录窗口内每个字符的出现次数
for right in range(len(s)):
char_frequency[s[right]] += 1 # 更新右边界字符的出现次数
# 当窗口内不同字符的数量超过k时,移动左边界
while len(char_frequency) > k:
char_frequency[s[left]] -= 1 # 减少左边界字符的出现次数
# 如果某个字符的出现次数变为0,从哈希表中移除它
if char_frequency[s[left]] == 0:
del char_frequency[s[left]]
left += 1 # 移动左边界
max_length = max(max_length, right - left + 1) # 更新最大子串长度
return max_length
424. 替换后的最长重复字符
题目描述
给你一个字符串
s和一个整数k。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行k次。在执行上述操作后,返回包含相同字母的最长子字符串的长度。示例 1:
输入: s = "ABAB", k = 2
输出: 4
解释: 用两个'A'替换为两个'B',反之亦然。
提示:
1 <= s.length <= 105s仅由大写英文字母组成0 <= k <= s.length
解法
这里我们使用Counter来记录每个字符的出现次数。我们可以写一个函数判断窗口内的字符串是否符合要求:
from collections import Counter
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left, right = 0, 0
max_length = 0
char_freq = Counter()
def is_valid(char_freq, right, left, k):
max_freq = max(char_freq.values())
res = right - left + 1 - max_freq
return res <= k
for right in range(len(s)):
char_freq[s[right]] += 1
while not is_valid(char_freq, right, left, k):
char_freq[s[left]] -= 1
left += 1
max_length = max(max_length, right-left+1)
return max_length
3. 无重复字符的最长子串
题目描述
给定一个字符串
s,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
提示:
0 <= s.length <= 5 * 104s由英文字母、数字、符号和空格组成
解法
from collections import defaultdict
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left, right = 0, 0
max_length = 0
# char_set = set()
char_freq = defaultdict(int)
while right<len(s):
char_freq[s[right]] += 1
while char_freq[s[right]] > 1:
char_freq[s[left]] -= 1
left += 1
max_length = max(max_length, right-left+1)
right += 1
return max_length
这里的一个巧妙之处在于我们维护的窗口都是没有重复字符的,所以当加入s[right]后,打破这一约束的唯一可能就是char_freq[s[right]]。所以,第二个while只需判断char_freq[s[right]] > 1即可。
438. 找到字符串中所有字母异位词
题目描述
给定两个字符串
s和p,找到s****中所有p****的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104s和p仅包含小写字母
解法
from collections import Counter
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
left, right = 0, 0
result = []
p_counter = Counter(p)
s_counter = Counter()
while right<len(s):
s_counter[s[right]] += 1
while right - left + 1 == len(p):
if s_counter == p_counter:
result.append(left)
s_counter[s[left]] -= 1
if s_counter[s[left]] == 0:
del s_counter[s[left]]
left += 1
right += 1
return result
这道题目的不同之处在于求所有符合条件的结果,所以这里使用一个列表来保存结果。
30. 串联所有单词的子串
题目描述
给定一个字符串
s****和一个字符串数组words。words中所有字符串 长度相同。s****中的 串联子串 是指一个包含words中所有字符串以任意顺序排列连接起来的子串。
例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。返回所有串联子串在 s ****中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入: s = "barfoothefoobarman", words = ["foo","bar"]
输出: [0,9]
解释: 因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
提示:
1 <= s.length <= 1041 <= words.length <= 50001 <= words[i].length <= 30words[i]和s由小写英文字母组成
解法
本以为这道题目就是438. 找到字符串中所有字母异位词的简单变形,结果抄过来改了一下怎么样都不对,最后debug了一下才找到原因,先看看我写过来的原始代码:
from collections import Counter
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
result = []
word_length = len(words[0])
w_counter = Counter(words)
for offset in range(len(words[0])):
strings =s[offset:]
left, right = 0, 0
s_counter = Counter()
while right<len(strings):
word = strings[right: right+word_length]
s_counter[word] += 1
while right-left == len(words)*len(words[0]):
start_word = strings[left: left+word_length]
# print("===========================")
# print("s[left: right]: ", strings[left: right])
# print("start_word: ", start_word)
# print("s_counter: ", s_counter)
# print("w_counter: ", w_counter)
# print("===========================")
if s_counter == w_counter:
result.append(left+offset)
s_counter[start_word] -= 1
if s_counter[start_word] == 0:
del s_counter[start_word]
left += word_length
right += word_length
return result
这里犯了好几个错误:
word = strings[right: right+word_length] s_counter[word] += 1 之后,导致s_counter里实际上是left到right+word_legth里的内容,而下面while还是判断的left到right! right实际上是区间的后面的下标+1,所以外层的while应该为right<=len(strings), 修改后的代码为:
from collections import Counter
from typing import List # 导入List类型,用于类型注解
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
result = [] # 存储结果的列表
word_length = len(words[0]) # 单个单词的长度
w_counter = Counter(words) # 统计words列表中单词的出现频率
# 由于所有单词长度相同,可以从s的不同偏移量开始检查
# 这样可以处理所有可能的子串
for offset in range(word_length):
strings = s[offset:] # 获得从偏移量开始的字符串
left, right = 0, 0 # 初始化左右指针
s_counter = Counter() # 存储当前检查的子串中单词的计数器
# 当右指针没有越界时继续
while right <= len(strings):
# 检查当前窗口大小是否与目标单词列表的总长度相同
while right - left == len(words) * word_length:
start_word = strings[left: left + word_length] # 当前窗口最左边的单词
# 如果计数器匹配,则找到一个结果
if s_counter == w_counter:
result.append(left + offset)
# 移动左指针以缩小窗口,并更新计数器
s_counter[start_word] -= 1
if s_counter[start_word] == 0:
del s_counter[start_word]
left += word_length
# 从当前右指针位置取出一个单词,并更新计数器和右指针
word = strings[right: right + word_length]
s_counter[word] += 1
right += word_length
return result
整体的思路就是最外层for循环进行一个单词长度的位置,相当于便利各种字符串起始位置的情况(因为s并不总是word长度的倍数),之后套用之前的438. 找到字符串中所有字母异位词代码,做一些修改即可。
76. 最小覆盖子串
题目描述
给你一个字符串
s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串""。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
解释: 最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s和t由英文字母组成
进阶: 你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
解法
整体思路与424. 替换后的最长重复字符类似,我们需要使用一个函数来统计当前窗口是否包含 t 的所有字符(包括重复字符)。
from collections import defaultdict, Counter # 导入所需的库
class Solution:
def minWindow(self, s: str, t: str) -> str:
start = 0 # 初始化滑动窗口的起始索引
char2count = defaultdict(int) # 创建一个默认字典来存储 s 中各字符的计数
t_count = Counter(t) # 使用 Counter 对象计算 t 中各字符的计数
# 初始化最小窗口长度和最小窗口的起始和结束索引
min_length = len(s) + 1
min_start, min_end = -1, -1
# 定义一个内部函数用于检查当前窗口是否包含 t 的所有字符(包括重复字符)
def check(char2count, t_count):
for char, count in t_count.items(): # 遍历 t 中的每个字符及其出现次数
if char2count[char] < count: # 如果 s 中该字符的计数小于 t 中的计数,则返回 False
return False
return True # 如果所有字符的计数都符合要求,则返回 True
# 遍历 s 中的每个字符
for end in range(len(s)):
char2count[s[end]] += 1 # 增加当前字符的计数
# 使用 while 循环不断缩小窗口,直到窗口不再包含 t 的所有字符
while check(char2count, t_count):
# 检查当前窗口是否比已找到的最小窗口还要小
if end - start + 1 < min_length:
min_length = end - start + 1
min_start, min_end = start, end # 更新最小窗口的起始和结束索引
# 缩小窗口(即移动窗口的起始索引)
char2count[s[start]] -= 1
start += 1
# 根据最小窗口的长度判断是否找到了符合条件的子串
if min_length == len(s) + 1:
return ""
else:
return s[min_start: min_end + 1] # 返回找到的最小窗口子串