目录
推荐一篇很牛的文章:讨论 - 力扣(LeetCode)
滑动窗口
是什么?
滑动窗口(Sliding Window)是一种高效的算法技巧,常用于数组或字符串的问题中,用于寻找满足特定条件的子数组或子串。
核心思想
可以简单理解为快慢指针的变种。
- 使用两个指针(通常是 left 和 right )定义一个“窗口”。
- right 指针向右扩展窗口,添加新元素。
- 当窗口不满足条件时, left 指针向右移动,缩小窗口。
- 通过这种方式,避免重复计算,实现O(n)时间复杂度。每次添加或删除元素的时间复杂度都是O(1)
实际案例分享
长度最小子数组(非定长)
题目来源:209. 长度最小的子数组 - 力扣(LeetCode)
首个题解:
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
res = float('inf')
left = 0
for right in range(len(nums)):
while sum(nums[left : right + 1]) >= target:
res = min(right + 1 - left,res)
left += 1
res = 0 if res == float('inf') else res
return res
Solution (O(n²) 时间复杂度) :在 while 循环内每次调用 sum(nums[left : right + 1]) ,这会重新计算整个子数组的和,导致嵌套循环,时间开销巨大。当 n=10^5 时,可能超时。
所以退化成了暴力破解
知识点:内置函数在数组数据很大时会很耗时
正解:
Solution (O(n) 时间复杂度) :使用累积和 s 维护窗口和,每次滑动只加减一个元素,高效。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
res = float('inf')
s = left = 0
for right in range(len(nums)):
s += nums[right]
while s >= target:
res = min(right + 1 - left,res)
s -= nums[left]
left += 1
res = 0 if res == float('inf') else res
return res
无重复字符的最长长度(非定长)
题目来源:3. 无重复字符的最长子串 - 力扣(LeetCode)
解:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
res = 0
left = 0
no_double = []
if len(s) == 1:
return 1
for right,s_ in enumerate(s):
while s_ in no_double:
no_double.remove(s[left])
left += 1
res = max(res,right + 1 - left)
no_double.append(s_)
return res
思路:
- 利用左右端点动态滑动窗口
- 如果该窗口内不存在重复字符,那么右端点右移(将该字符加入容器)
- 如果该窗口内存在重复字符,那么左端点右移(从容器中删除左端点对应的字符,左端点➕1)
-
注意:字符需要存储在一个容器中,这里我使用的是列表 in 操作判断是否已经存在,如果是那么就是在右端点右移后出现了重复字符
心得:
不是所有程序都可以一边写对,所以在有思路之后应该尝试coding,并不断debug修改直到完成!
优解:
利用计数器哈希表,记录每个字符出现的次数,如果次数大于等于2就是重复了,需要去重并右移端点
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
res = 0
left = 0
cnt = Counter()
for right,s_ in enumerate(s):
cnt[s_] += 1
while cnt[s_] >= 2:
cnt[s[left]] -= 1
left += 1
res = max(res,right + 1 - left)
return res
定长子串元音的最大数目 (定长)
题目来源:1456. 定长子串中元音的最大数目 - 力扣(LeetCode)
我的分析:
- 滑动窗口定长为k,使用两个指针标记窗口的左右端点,那么两个端点之间的长度应该为k,即right - left + 1 = k,由此可以推导left = right + 1 - k
- 滑动窗口我习惯让右端点遍历滑动,所以第一点公式基于此
- 由于需要记录窗口内的元音个数,当时脑子捋顺不过来了,就使用tmp存储窗口内元音数量,然后每更新一次就添加到ans列表,最后返回max(ans)
- 滑动窗口关键点就是如何移动,如何移除,所以不可能不调试就能一次写完的,除非像灵茶山这样的大佬,要利用好本地的debug工具梳理逻辑,然后修改不断完善并最终实现移动移除操作
我的解:
class Solution:
def maxVowels(self, s: str, k: int) -> int:
yuan = 'aeiou'
left = 0
ans = []
tmp = 0
for right in range(len(s)):
if s[right] in yuan:
tmp += 1
left = right +1 - k
if left < 0:
continue
ans.append(tmp) #这里可以直接 ans = max(ans,tmp)当时脑子抽抽了
if s[left] in yuan:
tmp -= 1
return max(ans)
大佬的解答:
class Solution:
def maxVowels(self, s: str, k: int) -> int:
ans = vowel = 0
for i, c in enumerate(s):
if c in "aeiou":
vowel += 1
if i < k - 1:
continue
ans = max(ans, vowel)
if s[i - k + 1] in "aeiou":
vowel -= 1
return ans
作者:灵茶山艾府
总结:
其实我的代码结构是没问题的,但是在写代码过程中脑子有点小乱,所以没有及时发现max(ans,tmp)方法
子数组最大平均数I
题目来源:643. 子数组最大平均数 I - 力扣(LeetCode)
我的分析:
- 定长滑动窗口,加数----->更新ans----->移除末尾数
- 需要注意的是,整数数组中存在负数,所以ans定义不能是0然后使用max获取,而是要定义为无穷小,即-inf或-float('inf')
我的解:
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
ans = -float('inf')
left = 0
tmp = 0
for right in range(len(nums)):
tmp += nums[right]
left = right + 1 - k
if left < 0:
continue
ans = max(ans,tmp/k)
tmp -= nums[left]
return ans
大小为k且平均值大于等于阈值的子数组
滑动窗口介绍见:算法学习--滑动窗口-CSDN博客
解答:
class Solution:
def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
ans = left = 0
tmp = 0
for right in range(len(arr)):
tmp += arr[right]
left = right + 1 - k
if left < 0:
continue
if tmp / k >= threshold:
ans += 1
tmp -= arr[left]
return ans
2090. 半径为 k 的子数组平均值
题目来源:2090. 半径为 k 的子数组平均值 - 力扣(LeetCode)
题目:
分析:
- 由于半径为k,那么window就是 2 *k + 1,所以这是一个定长滑动窗口
- 那么定长滑动窗口端点指针的关系就是 :
- right - left +1 = 2*k +1
- left = right - 2*k
- 由于i索引前后半径不够k avg直接赋值为-1:
- 如果len(nums) < len(window) return [i for i in range(len(nums))]
- 如果条件一满足,那么left < 0 or right > len(nums) -1 时,avg = -1
- 因为right作为window右端点,那么如果加和随着索引i相加就会遗漏sum(nums[k:right + 1]),所以我首先想到了随right索引加和,将加和结果变量初始化为sum(nums[:k]),这样就可以实现滑动窗口的移动和移除
我的解:
class Solution:
def getAverages(self, nums: List[int], k: int) -> List[int]:
# 窗口长度为 2k + 1
windows = 2*k + 1
ans = list()
length = len(nums)
# 前或后不足 k 个元素 半径为 k 的子数组平均值 是 -1
if length < windows:
return [-1 for i in range(length)]
left = 0
# 初始化
add = sum(nums[:k])
for i in range(length):
right = i + k
left = right - 2*k
if right >= length:
ans.append(-1)
continue
else:
add += nums[right]
if left < 0 :
ans.append(-1)
continue
# 更新 ans 数组
ans.append(add//len(nums[left:right+1]))
# 移除 windows末端数值
add -= nums[left]
return ans
题解:
class Solution:
def getAverages(self, nums: List[int], k: int) -> List[int]:
avgs = [-1] * len(nums)
s = 0 # 维护窗口元素和
for i, x in enumerate(nums):
# 1. 进入窗口
s += x
if i < k * 2: # 窗口大小不足 2k+1
continue
# 2. 记录答案
avgs[i - k] = s // (k * 2 + 1)
# 3. 离开窗口
s -= nums[i - k * 2]
return avgs
结论:
- avgs = [-1] * len(nums) 初始化只需要O(1),后面遍历处理的数据量小于n
- 我的代码中初始化求sum以及根据窗口判断更新ans都会增加耗时
2379. 得到 K 个黑块的最少涂色次数
题目来源:2379. 得到 K 个黑块的最少涂色次数 - 力扣(LeetCode)
解答:
class Solution:
def minimumRecolors(self, blocks: str, k: int) -> int:
ans = inf
left = 0
length = len(blocks)
data = {'W':0,'B':0}
for right in range(length):
data[blocks[right]] += 1
left = right + 1 - k
if left < 0:
continue
ans = min(ans,data['W'])
data[blocks[left]] -= 1
return ans
2841. 几乎唯一子数组的最大和
题目来源:2841. 几乎唯一子数组的最大和 - 力扣(LeetCode)
分析:
- 定长滑动窗口 right遍历元素,left = rigth + 1 - k(通过定长k推导,right - left + 1 = k)
- 除了窗口固定外,还需要保证窗口内的不同元素数 >= m,我选择使用字典来存储,当nums[left]在字典中value == 1的时候,需要将该元素移除,如果大于1,那么value减去1,不同元素数量不变
我的解:
class Solution:
def maxSum(self, nums: List[int], m: int, k: int) -> int:
ans = 0
if len(nums) < k:
return 0
dic = dict()
# m 必然小于 k
ans_tmp = 0
m_tmp = 0
for right in range(len(nums)):
ans_tmp += nums[right]
if nums[right] not in dic:
dic[nums[right]] = 0
m_tmp += 1
dic[nums[right]] += 1
left = right + 1 - k
if left < 0:
continue
if m_tmp >= m:
ans = max(ans,ans_tmp)
ans_tmp -= nums[left]
if dic[nums[left]] == 1:
dic.pop(nums[left])
m_tmp -= 1
else:
dic[nums[left]] -= 1
return int(ans)
题解:
class Solution:
def maxSum(self, nums: List[int], m: int, k: int) -> int:
ans = s = 0
cnt = defaultdict(int)
for i, x in enumerate(nums):
# 1. 进入窗口
s += x
cnt[x] += 1
left = i - k + 1
if left < 0: # 窗口大小不足 k
continue
# 2. 更新答案
if len(cnt) >= m:
ans = max(ans, s)
# 3. 离开窗口
out = nums[left]
s -= out
cnt[out] -= 1
if cnt[out] == 0:
del cnt[out]
return ans
作者:灵茶山艾府
2461. 长度为 K 子数组中的最大和
题目来源:2461. 长度为 K 子数组中的最大和 - 力扣(LeetCode)
我的解:
class Solution:
def maximumSubarraySum(self, nums: List[int], k: int) -> int:
from math import inf
dic = defaultdict(int)
left = 0
ans = -inf
ans_tmp = 0
# 存储不同元素数
element = 0
for right,num in enumerate(nums):
if dic[num] == 0:
element += 1
ans_tmp += num
dic[num] += 1
left = right + 1 - k
if left < 0:
continue
if element == k:
ans = max(ans,ans_tmp)
if dic[nums[left]] > 1:
dic[nums[left]] -= 1
elif dic[nums[left]] == 1:
element -= 1
dic[nums[left]] -= 1
ans_tmp -= nums[left]
if ans == -inf :
return 0
return ans
题解:
class Solution:
def maximumSubarraySum(self, nums: List[int], k: int) -> int:
ans = s = 0
cnt = defaultdict(int)
for i, x in enumerate(nums):
# 1. 进入窗口
s += x
cnt[x] += 1
left = i - k + 1
if left < 0: # 窗口大小不足 k
continue
# 2. 更新答案
if len(cnt) == k:
ans = max(ans, s)
# 3. 离开窗口
out = nums[left]
s -= out
cnt[out] -= 1
if cnt[out] == 0:
del cnt[out]
return ans
作者:灵茶山艾府
总结:
1. 不同元素的个数完全可以通过len(dic)来获取,没必要单独搞一个变量,同时不用担心时间开销,因为len操作时间复杂度为O(1)
2. 要常看题解
1423. 可获得的最大点数
思路:
- 从所有能够取到的长度为k的子数组中进行遍历,返回子数组最大和
- 能够取到的子数组根据题目条件判断
我的解:
class Solution:
def maxScore(self, cardPoints: List[int], k: int) -> int:
s = sum(cardPoints[:k])
right = 0
ans = s
if len(cardPoints) <= k:
return sum(cardPoints)
for left in range(k - 1, -1 ,-1):
right = left + 1 - k
s += cardPoints[right - 1]
s -= cardPoints[left]
ans = max(ans,s)
ans = max(ans,sum(cardPoints[-k:]))
return ans
1052. 爱生气的书店老板
题目来源:1052. 爱生气的书店老板 - 力扣(LeetCode)
分析:
- 求不生气的时间最多顾客数量
- 将原始本来就不生气的时间内所有顾客累加 res
- 通过滑动grumpy来获取,在连续minutes时间内不生气导致的满意顾客增量 ans
- 最后 res += ans即时结果
答案:
class Solution:
def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int:
'''
customers = [1,0,1,2,1,1,7,5],
grumpy = [0,1,0,1,0,1,0,1],
minutes = 3
遍历grumpy 滑动长度为3窗口 求生气窗口最大增值
所有不生气的时间都顾客数量 加上 最大增值 即时最后结果
'''
# 增值
ans = ans_tmp = 0
left = 0
res = 0
for right,gru in enumerate(grumpy):
# -----> 移入
if gru == 0:
res += customers[right] # 加和所有不生气的人数
# 计算窗口增值
if gru == 1:
ans_tmp += customers[right]
left = right + 1 - minutes
if left < 0 :
continue
# 更新增值 以及对应的窗口左端点 作为该窗口标识
if ans < ans_tmp:
ans = max(ans,ans_tmp)
# -----------> 移除
if grumpy[left] == 1:
tmp = customers[left]
ans_tmp -= tmp
res += ans
return res
灵神:
class Solution:
def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int:
s = [0, 0]
max_s1 = 0
for i, (c, g) in enumerate(zip(customers, grumpy)):
s[g] += c
if i < minutes - 1: # 窗口长度不足 minutes
continue
max_s1 = max(max_s1, s[1])
if grumpy[i - minutes + 1]:
s[1] -= customers[i - minutes + 1] # 窗口最左边元素离开窗口
return s[0] + max_s1
作者:灵茶山艾府
1652. 拆炸弹
分析:
1. 窗口固定为k 只不过移动方向随k对正负变化
2. 我区分了 k > 0 and k < 0 的情况
3. 在不同方向上进行逻辑移动,不用方向也移动的逻辑还不同,所以花费了比较多的时间来做这道“简单题”
我的解答:
class Solution:
def decrypt(self, code: List[int], k: int) -> List[int]:
if k == 0:
return [0 for i in range(len(code))]
if k > 0:
ans = []
ans_tmp = 0
last_num = 0
left = 0
for right,num in enumerate(code):
# in
ans_tmp += num
left = right + 1 - k
if left < 0:
continue
# update
if left == 0:
last_num = ans_tmp
else:
ans.append(ans_tmp)
# out
ans_tmp -= code[left]
for right in range(k - 1):
# in
ans_tmp += code[right]
left = right + 1 - k
ans.append(ans_tmp)
ans_tmp -= code[left]
ans.append(last_num)
return ans
else:
ans = [0 for i in range(len(code))]
ans_tmp = 0
left = -1
for right in range(-1,-len(code),-1):
# in
ans_tmp += code[right]
left = right - 1 - k
if left >= 0:
continue
ans[left + 1] = ans_tmp
ans_tmp -= code[left ]
for right in range(0,k,-1):
ans_tmp += code[right]
left = right - k
ans[left] = ans_tmp
ans_tmp -= code[left - 1]
return ans
题解:
class Solution:
def decrypt(self, code: List[int], k: int) -> List[int]:
n = len(code)
ans = [0] * n
r = k + 1 if k > 0 else n # 第一个窗口的右开端点
k = abs(k)
s = sum(code[r - k: r]) # ans[0]
for i in range(n):
ans[i] = s
s += code[r % n] - code[(r - k) % n]
r += 1
return ans
总结:
1. 找规律
2. 我想我没有找到题解这个规律,应该是刷的题目,太少了,就导致解题思路以及思维方式比较局限
3. 题解上面还有一个作者的分析思路,我认为该分析方法十分重要,通过手绘来寻找算法规律,然后开始敲代码