- 3. Longest Substring Without Repeating Characters">3. Longest Substring Without Repeating Characters
- 76. Minimum Window Substring">76. Minimum Window Substring
- 643. Maximum Average Subarray I">643. Maximum Average Subarray I
- 567. Permutation in String">567. Permutation in String
- 239. Sliding Window Maximum">239. Sliding Window Maximum
- 2024. Maximize the Confusion of an Exam">2024. Maximize the Confusion of an Exam
- 1004. Max Consecutive Ones III">1004. Max Consecutive Ones III
- 713. Subarray Product Less Than K">713. Subarray Product Less Than K
3. Longest Substring Without Repeating Characters
以右指针为基准遍历扩大窗口,每次加入字符后判断是否出现重复,若是则不断移动左指针缩小窗口。
class Solution:def lengthOfLongestSubstring(self, s: str) -> int:res = 0dic = set()l = 0for r, c in enumerate(s):while c in dic:dic.remove(s[l])l += 1dic.add(c)res = max(res, r - l + 1)return res
- 时间复杂度:
- 空间复杂度:
76. Minimum Window Substring
使用freq记录每个字符对应的还需要填充频数,cnt记录子串t中字符还需要填充的总频数。
需要注意在扩大和缩小窗口时,不要忘记改变cnt的判断。
特殊用例:s == "a" t == "a"
故不可将min_len设置为len(s)
from math import inffrom collections import Counterclass Solution:def minWindow(self, s: str, t: str) -> str:freq = Counter(t) # window中还需要的char数量cnt = len(t) # window中还需要的char数量总和res, min_len = '', infl = 0for r, c in enumerate(s): # 扩大窗口,尝试覆盖tif freq[c] > 0: # 判断是否是t中字符cnt -= 1freq[c] -= 1while cnt == 0: # 已完全覆盖子串,开始缩小窗口if (cur_len := r-l+1) < min_len:min_len = cur_lenres = s[l:r+1]if freq[s[l]] == 0: # 判断是否是t中字符cnt += 1freq[s[l]] += 1l += 1return res
- 时间复杂度:
- 空间复杂度:
643. Maximum Average Subarray I
- 滑动窗口裸题
- 初始化将滑动窗口压满,取得第一个滑动窗口的目标值
- 继续滑动窗口,每往前滑动一次,需要删除一个和添加一个元素
前缀和(次优解)
- substring子串,subarray子数组: 连续
subsequence子序列: 不一定连续
class Solution:def findMaxAverage(self, nums: List[int], k: int) -> float:res = window = sum(nums[:k])for i in range(k, len(nums)):window += nums[i] - nums[i-k]res = max(res, window)return res / k
时间复杂度:
- 空间复杂度:
567. Permutation in String
限定了字符串只包含小写字母,故使用两个长度为26的频数数组来记录。
class Solution:def checkInclusion(self, s1: str, s2: str) -> bool:m, n = len(s1), len(s2)if m > n:return Falsecnt_1 = [0] * 26cnt_2 = [0] * 26for i in range(m): # 压满窗口(以s1为基准)cnt_1[ord(s1[i]) - ord('a')] += 1cnt_2[ord(s2[i]) - ord('a')] += 1if cnt_1 == cnt_2:return Truefor i in range(m, n): # 向右移动窗口cnt_2[ord(s2[i]) - ord('a')] += 1cnt_2[ord(s2[i-m]) - ord('a')] -= 1if cnt_1 == cnt_2:return Truereturn False
- 时间复杂度:
- 空间复杂度:
239. Sliding Window Maximum
Approach I** 优先队列(堆)
使用一个大顶堆记录窗口中的最大值:每次首先加入元素(取反是因为Python的默认heapq实现是小顶堆)与索引,然后循环**判断堆顶元素是否在窗口内(只能删除堆顶元素来保证堆性质,循环删除是因为某个较大元素而导致累积了较多的元素在窗口内)。
import heapqclass Solution:def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:max_heap = [(-nums[i], i) for i in range(k)] # 记录index:heapq.heapify(max_heap) # 为了判断堆顶元素是否在窗口中res = [-max_heap[0][0]]for i in range(k, len(nums)):heapq.heappush(max_heap, (-nums[i], i))while max_heap[0][1] <= i-k: # 循环:堆顶元素不在窗口,可永久删除heapq.heappop(max_heap) # 使用while而非ifres.append(-max_heap[0][0])return res
- 时间复杂度:
,每次调整堆
- 空间复杂度:
,注意到可能会有冗余元素(不在窗口中)存在堆中,故空间复杂度不是
Approach II** 单调队列(双端队列deque实现)
考虑滑动窗口内的两个下标和
,如果
并且
,那么在窗口右移的过程中,只要
还在窗口中,
就必定在窗口中,那么
的存在就无关紧要了,故可以删去。
考虑使用一个严格单调递减的双端队列,保存数组的索引。在保证队列的最大长度为的情况下,队首元素便是窗口内最大值的索引。
为什么不用单调栈,而是使用单调队列?
本题中有两种弹出数据结构内元素的策略:其一,从右侧弹出,为了保证单调性;其二,从左侧弹出,为了保证窗口大小。故只有双端队列这种数据结构能够满足题设需求。
为何队列中保存索引,而不是数值?
这是为了存储更多信息**(类似的,单调栈问题也有相同的操作)。有了索引便可以求得对应下标的数值,可以同时完成上一个问题中两个弹出元素的判断过程。
from collections import dequeclass Solution:def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:queue = deque() # 严格单调递减 双端队列:保存indexfor i in range(k): # 初始化:压满滑动窗口while queue and nums[queue[-1]] <= nums[i]:queue.pop()queue.append(i)res = [nums[queue[0]]]for i in range(k, len(nums)): # 滑动,扩大窗口while queue and nums[queue[-1]] <= nums[i]: # 维持单调性queue.pop()queue.append(i)if queue[0] <= i-k: # 保存index的原因:判断队首元素是否不在窗口中queue.popleft() # 缩小窗口res.append(nums[queue[0]])return res
- 时间复杂度:
- 空间复杂度:
,极限情况队列保存
个元素
2024. Maximize the Confusion of an Exam
题目求修改次数不超过k时,连续子串T或者F的最大长度;
等价于求包含T或F个数不超过k时的连续子串最大长度,使用滑动窗口。
class Solution:def maxConsecutiveAnswers(self, answer: str, k: int) -> int:def max_consecutive_window(ch):res = 0cnt = 0 # window中包含ch的数量l = 0for r in range(len(answer)):cnt += answer[r] == chwhile cnt > k:cnt -= answer[l] == chl += 1res = max(res, r-l+1)return resreturn max(max_consecutive_window('T'), max_consecutive_window('F'))
- 时间复杂度:
- 空间复杂度:
1004. Max Consecutive Ones III
Approach I** 动态规划(超时) => :考虑前
个
num(并以结尾),最大翻转次数为
,连续
的最大长度。
,无需消耗反转次数,
,由于计算连续
的最大长度必须以
结尾,故消耗一次翻转次数,
的数量级不超过
的话,动态规划是可行的。
class Solution:def longestOnes(self, nums: List[int], k: int) -> int:n = len(nums)res = 0dp = [[0] * (k+1) for _ in range(2)]for i in range(1, n+1):for j in range(k+1):if nums[i-1] == 1:dp[i&1][j] = dp[(i-1)&1][j] + 1else:dp[i&1][j] = 0 if j == 0 else dp[(i-1)&1][j-1] + 1res = max(res, dp[i&1][j])return res
- 时间复杂度:
- 空间复杂度:
Approach II** 前缀和+双指针 => 对数**
Approach III** 滑动窗口 => 常数**
class Solution:def longestOnes(self, nums: List[int], k: int) -> int:res = 0cnt = 0 # 窗口中0的数量l = 0for r, num in enumerate(nums):cnt += num == 0while cnt > k:cnt -= nums[l] == 0l += 1res = max(res, r-l+1)return res
- 时间复杂度:
- 空间复杂度:
713. Subarray Product Less Than K
class Solution:def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:if k <= 1:return 0res = 0 # 记录乘积<k且以右指针j结尾的子数组prod = 1i = 0for j in range(len(nums)):prod *= nums[j]while prod >= k:prod //= nums[i]i += 1res += j - i + 1return res
- 时间复杂度:
- 空间复杂度:
