概念 & 应用
sliding window可以解决数组 or 字符串的子元素问题:
- 将嵌套的循环问题,转换为单循环问题 -> 降低时间复杂度,一般为
O(N) - 应用:
- 题干:
- 连续的元素:string / subarray / linkedList
- 最值:min / max / longest / shortest / keyword
- 一般 string 使用 map 作为window(如果说明了只有小写字母也可以使用
int[26]) - 字母类可若暴力尝试 26 个字母(比如1-unique,2-unque,……),然后套模版(ie:LC395)
- 套娃题:
exact(K) = atMost(K) - atMost(K-1) - 多重限制的压轴题需要考虑是否为
单调队列
- 题干:
模版
- 本质上仍是
two pointer:左边left& 右边right = iterator(i) - 三步走:进 -> 出 -> 算
❤️❤️ Longest:
while 不符合条件: left+=1❤️❤️ Shortest:
if 符合条件: while left指向的元素还可以删除: 删除left指向的元素 left+=1
class Solution:
# 本质上仍是two pointer:左边left,右边iterator(i)
def lengthOfLongestSubstringDistinct(self, s: str, k: int) -> int:
dic = {}
l, r, res = 0, 0, 0
for i, c in enumerate(s):
dic[c] = dic.get(c, 0) + 1 # 1 进:当前遍历的i进入窗口,更新map
while len(dic) > k: # 2 出:当窗口不符合条件时,left持续退出窗口
dic[s[l]] -= 1
if dic[s[l]] == 0:
del dic[s[l]]
l += 1
res = max(res, i - l + 1) # 3 算:现在窗口valid了,计算结果
return res
*题型1:size fixed(Easy)
- 窗口长度确定
- 比如:max sum of
size = k
- 比如:max sum of
*题型2:size可变,单限制条件(Medium)
- 比如找到subarray
sum 比target大一点点
❤️ 题型3:size可变,双限制条件(Medium)
3. 无重复字符的最长子串(Medium)
Solu 1:Set去重
套模版,略
- 因为只要求distinct(没有frequency的要求),直接用
set就可以
Code 1:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
seen = set()
l, res = 0, 0
for i, c in enumerate(s):
while c in seen:
seen.remove(s[l])
l += 1
seen.add(c)
res = max(res, i - l + 1)
return res
Solu 2:dictionary优化 ❤️
- dict =
<current character : cur上一次出现的idx>
Code 2:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
dic = {}
max_len, l = 0, 0
# left = 上一次current character出现的index
for i, c in enumerate(s):
if c in dic: # cur之前就出现过,但不确定是否在当前窗口内
l = max(l, dic[c] + 1)
dic[c] = i
max_len = max(max_len, i - l + 1)
return max_len
159. 至多包含两个不同字符的最长子串(Medium)
Solu:
套模版,k = 2
- 进:当前character进入map
- 出:移除window内多余的character
- 算:计算长度结果
Code:
class Solution:
def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int:
dic = {}
l, res = 0, 0
for i, c in enumerate(s):
dic[c] = dic.get(c, 0) + 1 # 1 进
while len(dic) > 2:
dic[s[l]] -= 1 # 2 出
if dic[s[l]] == 0:
del dic[s[l]]
l += 1
res = max(res, i - l + 1) # 3 算
return res
340. 至多包含 K 个不同字符的最长子串(Medium)
Solu:
- dic =
<cur_character : frequency of cur> - 直接把
len(dic)和k做比较即可
Code:
class Solution:
def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
dic = {}
l, res = 0, 0
for r, c in enumerate(s):
dic[c] = dic.get(c, 0) + 1
while len(dic) > k:
dic[s[l]] -= 1
if dic[s[l]] == 0:
del dic[s[l]]
l += 1
res = max(res, r - l + 1)
return res
76. 最小覆盖子串(Hard)
Solu:
- 饱和count的valid subarray不值得去计算(即:
#char in s[l:r] > #char in t)。只考虑对于当前left,第一次遇到valid subarrays[left, right]时的case:再进行收缩窗口&计算长度
Code:
class Solution:
def minWindow(self, s: str, t: str) -> str:
dic = collections.Counter(t) # <character - 还剩多少个需要抵消>
l, ans, k = 0, '', len(t)
for r, c in enumerate(s):
if c in dic:
if dic[c] > 0: # cur还没抵消完
k -= 1
dic[c] -= 1
if k == 0: # valid substring
while k == 0: # 收缩窗口
if s[l] in dic:
dic[s[l]] += 1
if dic[s[l]] > 0: # s[l]没有抵消完
k += 1
l += 1
if r - l + 1 < len(ans) or not ans:
ans = s[l - 1:r + 1]
return ans
❤️ 395. 至少有 K 个重复字符的最长子串(Medium)
Solu:
def longest_substring_with_n_unique(s: str, k: int, unique: int) -> int: 找出s中的最长子串, 要求该子串具有unique个unique character 且 其中的每一字符出现frequency ≥ k- 最多有26个unique character ->
longest_substring(s: str, n: int) = max{longest_substring_with_n_unique(s, k, unique)} (1 ≤ unique ≤ #distinct char in s)- 关心的是一个char最少出现k次,所以只有第
k次出现这个char的时候才需要count
- 关心的是一个char最少出现k次,所以只有第
Code:
class Solution:
def longestSubstring(self, s: str, k: int) -> int:
def longestSubstringNUnique(unique: int) -> int:
'''
Find the longest substring with exactly unique characters, and each distinct character is at least k times.
:param unique: number of unique characters
:return: length of longest substring
'''
dic = {}
l, res, validCount = 0, 0, 0
for r, c in enumerate(s):
dic[c] = dic.get(c, 0) + 1 # <char : freq>
if dic[c] == k:
validCount += 1 # 第k次出现某个unique char 的 次数
while len(dic) > unique: # 保证当前unique char 的个数不超过'unique'
dic[s[l]] -= 1
if dic[s[l]] == k - 1:
validCount -= 1
if dic[s[l]] == 0:
del dic[s[l]]
l += 1
if validCount == unique:
res = max(res, r - l + 1)
return res
return max(longestSubstringNUnique(unique) for unique in range(1, len(collections.Counter(s)) + 1))
209. 长度最小的子数组(Medium)
Solu:
- 保持
sum = ∑(s[l:r])(r= 当前for-loop到的idx)
Code:
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
sum = 0
l, res = 0, len(nums) + 1
for r, num in enumerate(nums):
sum += num # 进
if sum >= target:
while sum - nums[l] >= target:
sum -= nums[l] # 出
l += 1
res = min(res, r - l + 1) # 算
return res if res <= len(nums) else 0
424. 替换后的最长重复字符(Medium)
Solu:
- 进:iterator不断前进
- 出:如果 #使得当前window符合条件所需要的改动次数 过多(即,
[window.size] - [window's most frequent char's frequency] > k),则不断收缩窗口左边界- 不用真的去修改string
- 算:最后计算最长值
Code:
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
count_array = [0] * 26
l, res = 0, 0,
for r, c in enumerate(s):
count_array[ord(c) - ord('A')] += 1 # 进
while (r - l + 1) - max(count_array) > k: # 出
count_array[ord(s[l]) - ord('A')] -= 1
l += 1
res = max(res, r - l + 1) # 算
return res
❤️ 1234. 替换子串得到平衡字符串(Medium)
Solu:滑动窗口
- 滑动窗口内:需要被替换的子串
- 滑动窗口外:符合条件的剩余字串(即:
#each character ≤ n/4) - 因为需要获得
min{len(被替换子串)},所以一旦窗口外的部分满足条件了,就尝试收缩窗口左边界
Code:
class Solution:
def balancedString(self, s: str) -> int:
def isValid(dic, taregt):
for k, v in dic.items():
if v > taregt:
return False
return True
count = collections.Counter(s)
target = len(s) // 4
l, res = 0, len(s)
for r, c in enumerate(s):
if isValid(count, target):
return 0
count[c] -= 1
while l <= r and isValid(count, target):
res = min(res, r - l + 1)
count[s[l]] += 1
l += 1
return res
❤️ 632. 最小区间(Hard)
Solu:排序 + HashMap + 滑动窗口
- 首先将
k组数据升序合并成一组,并记录每个数字所属的组- 例如:对于
[[4,10,15,24,26], [0,9,12,20], [5,18,22,30]][[4,10,15,24,26],[0,9,12,20],[5,18,22,30]],合并升序后可以得到:[(0, 1), (4, 0), (5, 2), (9, 1), (10, 0), (12, 1), (15, 0), (18, 2), (20, 1), (22, 2), (24, 0), (26, 0), (30, 2)][(0,1),(4,0),(5,2),(9,1),(10,0),(12,1),(15,0),(18,2),(20,1),(22,2),(24,0),(26,0),(30,2)]
- 例如:对于
- 然后只看所属组的话,那么有
[1, 0, 2, 1, 0, 1, 0, 2, 1, 2, 0, 0, 2][1,0,2,1,0,1,0,2,1,2,0,0,2] - 按组进行“滑动窗口”,找到满足正好窗口内有
k组的最小窗口- strategy:如果当前
l指向的group的数量仍>1,那么就代表还可以继续收缩窗口
- strategy:如果当前
Code:
class Solution:
def smallestRange(self, nums: List[List[int]]) -> List[int]:
numGroups = len(nums)
ordered = sorted((j, i) for i, points in enumerate(nums) for j in points) # val:groupID
dic = {}
l, res = 0, [ordered[0][0], ordered[-1][0]]
for r in range(len(ordered)):
dic[ordered[r][1]] = dic.get(ordered[r][1], 0) + 1
if len(dic) == numGroups:
while dic[ordered[l][1]] > 1: # 如果当前l指向的group的数量仍>1,那么就代表还可以收缩窗口
dic[ordered[l][1]] -= 1
l += 1
if ordered[r][0] - ordered[l][0] < res[1] - res[0]:
res = [ordered[l][0], ordered[r][0]]
return res
❤️ 套娃题:exact(K)
exactt(K) = atMost(K) - atMost(K-1)atMost(K)的累加过程当成longest来做!!
992. K 个不同整数的子数组(Hard)
Solu:
exactly(K) = atMost(K) - atMost(K-1)- 对于每个右边界r,设:
nums[l1 : r]是满足#distinct num ≤ k的最长的subarraynums[l2 : r]是满足#distinct num ≤ k-1的最长的subarray#以 r 为右边界的满足有正好 k 个不同数字的subarray=#nums[l : r](其中l1 ≤ l < l2)=l2 - l1exactly(K) = ∑(l2 - l1) = atMost(K) - atMost(K-1)
Code:
class Solution:
def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
def atMostK(unique: int) -> int:
dic = {}
l, res = 0, 0
for r, num in enumerate(nums):
dic[num] = dic.get(num, 0) + 1
while len(dic) > unique:
dic[nums[l]] -= 1
if dic[nums[l]] == 0:
del dic[nums[l]]
l += 1
res += r - l + 1 # nums[l:r+1]中所有的subarray有1,2,...,unique个不同的元素
return res
return atMostK(k) - atMostK(k - 1)
1248. 统计「优美子数组」(Medium)
Solu:
exactt(K) = atMost(K) - atMost(K-1)- atMost(K) = #至多有K个奇数的subarray
Code:
class Solution:
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
def atMostK(odd: int) -> int:
cnt, l, res = 0, 0, 0
for r, n in enumerate(nums):
cnt += n % 2
while cnt > odd:
cnt -= nums[l] % 2
l += 1
res += r - l + 1
return res
return atMostK(k) - atMostK(k - 1)
*题型4:size fixed,单限制条件(Hard)
- 比如sliding window maximum,考察
单调队列