这是我参与更文挑战的第18天,活动详情查看: 更文挑战
最近想把自己刷算法题的经验心得整理一下,一方面为了复习巩固,另一方面也希望我的分享能够帮助到更多在学习算法的朋友。
专栏名称叫《算法锦囊》,在讲解算法时会注重整体性,但不会面面俱到,适合有一定算法经验的人阅读。
这一次我们重点来看滑动窗口,这一部分的所有题目和源码都上传到了github的该目录下,题解主要用Python语言实现。
概述
滑动窗口指的是这样一类问题的求解方法,在数组上通过双指针同向移动而解决的一类问题。其实这样的问题我们可以不必为它们专门命名一个名字,它们的解法其实是很自然的。
使用滑动窗口解决的问题通常是暴力解法的优化,掌握这一类问题最好的办法就是练习,然后思考清楚为什么可以使用滑动窗口。
滑动窗口的解题模板可参考负雪明烛的这个答案。
模板如下:
def findSubArray(nums):
N = len(nums) # 数组/字符串长度
left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
while 区间[left, right]不符合题意:# 此时需要一直移动左指针,直至找到一个符合题意的区间
sums -= nums[left] # 移动左指针前需要从counter中减少left位置字符的求和/计数
left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
# 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
res = max(res, right - left + 1) # 需要更新结果
right += 1 # 移动右指针,去探索新的区间
return res
3. 无重复字符的最长子串
首先看这一题,3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
这是典型的滑动窗口的题目,维护left和right两个指针。不断向右移动right,找到满足条件的最大长度,如果不符合,则向右移动left,找到最大长度。
套用上面的模板,我们可以完美写出答案。我用了一个集合去存储当前已经遍历的值,如果right指针向右遍历时不满足条件,会从集合里先去掉该元素。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left = right = res = 0
window_set = set()
while right < len(s):
while s[right] in window_set:
window_set.remove(s[left])
left += 1
window_set.add(s[right])
res = max(res, right - left + 1)
right += 1
return res
159. 至多包含两个不同字符的最长子串
趁热打铁,我们继续看 159. 至多包含两个不同字符的最长子串
对于这道题,我们仍可以沿用上面的模板,与前面的不同的时,在判断滑动窗口何时增加和减少元素时,需要根据题目做不同的判断。
我这里使用了一个字典,来存储当前遍历过的元素的数目,一旦出现字典元素个数比2小,则左移left指针,并调整字典。
import collections
class Solution:
def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int:
dic = collections.defaultdict(int)
left = right = res = 0
while right < len(s):
dic[s[right]] += 1
while len(dic) > 2:
dic[s[left]] -= 1
if dic[s[left]] == 0:
del dic[s[left]]
left += 1
res = max(res, right-left+1)
right += 1
return res
这道题还有引申版本340. 至多包含 K 个不同字符的最长子串,不过掌握方法后,万变不离其宗。
给定一个字符串 s ,找出 至多 包含 k 个不同字符的最长子串 T。
示例 1:
输入: s = "eceba", k = 2
输出: 3
解释: 则 T 为 "ece",所以长度为 3。
class Solution:
def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
dic = collections.defaultdict(int)
left = right = res = 0
while right < len(s):
dic[s[right]] += 1
while len(dic) > k:
dic[s[left]] -= 1
if dic[s[left]] == 0:
del dic[s[left]]
left += 1
res = max(res, right-left+1)
right += 1
return res
424. 替换后的最长重复字符
我们接着来看424. 替换后的最长重复字符。
给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
注意:字符串长度 和 k 不会超过 104。
示例 1:
输入:s = "ABAB", k = 2
输出:4
解释:用两个'A'替换为两个'B',反之亦然。
这道题初看的时候,也许意识不到要用滑动窗口的解法。我们可以通过一个数组来记录每个元素的出现次数,取改数组的最大值,看滑动窗口的长度减去这个值的结果是否比k大,来判断是否符合题目要求。
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
num = [0] * 26
n = len(s)
ans = left = right = 0
while right < n:
num[ord(s[right]) - ord("A")] += 1
maxn = max(num)
while right - left + 1 - maxn > k:
num[ord(s[left]) - ord("A")] -= 1
maxn = max(num)
left += 1
ans = max(ans, right - left + 1)
right += 1
return ans
239. 滑动窗口最大值
最后来看这道经典的滑动窗口题目,239. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
这道题因为窗口是固定的,跟我们前面的几道题稍微有所区别。比较简洁的做法,利用一个双端队列来存储滑动窗口,并且每次放入元素的时候,保证最左边的元素永远是滑动窗口的最大值。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
size = len(nums)
# 特判
if size == 0:
return []
# 结果集
res = []
# 滑动窗口,注意:保存的是索引值
window = deque()
for i in range(size):
# 当元素从左边界滑出的时候,如果它恰恰好是滑动窗口的最大值
# 那么将它弹出
if i >= k and i - k == window[0]:
window.popleft()
# 如果滑动窗口非空,新进来的数比队列里已经存在的数还要大
# 则说明已经存在数一定不会是滑动窗口的最大值(它们毫无出头之日)
# 将它们弹出
while window and nums[window[-1]] <= nums[i]:
window.pop()
window.append(i)
# 队首一定是滑动窗口的最大值的索引
if i >= k - 1:
res.append(nums[window[0]])
return res