同向双指针——滑动窗口
套路讲解实例一:LeetCode209. 数组和 ≥ target 的最短子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
- 暴力双循环 时间复杂度O(n^2).
- 同向双指针
就举示例1的例子,设左右指针初始都为0,想要找>=7的最短子数组,可以枚举每个右端点,找到以每个数为右端点的最短数组。
在右指针向右遍历的过程中,若数组小于target=7则右指针继续向右移动
我们假设现在取到了2 3 1 2即2为右端点,这是大于等于7的,那么为了取到最短的数组,我们可以将左指针向右移动,这些就可以缩短数组,直到数组<7即取到3 1 2的情况,这时我们已经知道了以2为右端点的数组>=7最短为2 3 1 2 ,长度为4
之后我们继续遍历右指针,向右移动,此时左端点为3 ,且此时我们的左指针
不需要回退,因为我们已知 2 3 1 2>7,若左指针回退,而右指针却向右移动,则必然不是最短的子数组,故此时我们的数组为3 1 2 4>7,故向之前一样,左指针向右移动,直到数组<7即取到2 4的情况,这时我们已经知道了以4为右端点的数组>=7最短为1 2 4 ,长度为3
右指针继续向右,此时3为数组右端点,2为左端点, 此时我们判断数组2 4 3>7,那么我们和之前一样,向右移动左指针,直到<7,此时我们知道以3为右端点的数组最短为4 3。
这就是同向双指针的具体流程,右指针与左指针的移动距离都最多为n,故时间复杂度为O(n).
#数组中和大于等于target的最短子数组
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n = len(nums)
ans = n + 1 # 也可以写 inf
s = left = 0
for right, x in enumerate(nums):
s += x
while s >= target: # 满足要求时循环
ans = min(ans, right - left + 1)#满足条件,每次循环开始取最小值
s -= nums[left]
left += 1
return ans if ans <= n else 0
套路讲解实例二:数组中和为target的最长子数组
def maxSubArrayLen(target: int, nums: list[int]) -> int:
ans = -1
s = 0
left = 0
for right, x in enumerate(nums):
s += x
while s > target: # 不满足要求,循环
s -= nums[left]
left += 1
if s == target:
ans = max(ans, right - left + 1)
return ans
套路总结:
思路一:
指针移动对结果改变要具有单调性,即right或left右移只会导致数组单调增加或减少
若满足单调,则首先对right进行 for range枚举,并循环判断是否移动left,直到比较子数组与target满足条件,并取max或min
target条件可以有多种情况,比如子数组和、积、长度、元素出现次数等等
求最小值时,右指针移动会导致满足条件,故要通过左指针移动找出当前满足条件的右指针的最值,在左指针循环内先求最值min,不满足条件时退出当前左指针循环,继续遍历右指针
求最大值或总和时,右指针移动可能会导致不满足条件,故要通过左指针由不满足条件移动到满足条件,取退出循环后第一次满足条件的最值max,即在左指针循环退出后
思路二:
由于是对于right进行for range循环,故在for循环内部不能对right++,只能left++,故我们只需要考虑何种情况下进行left++,据此就能写出代码。
练习LeetCode713. 乘积小于 K 的子数组
给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。
按照套路,for range枚举右端点,乘积>=k时左端点右移,即左指针移动时从不满足条件到满足条件
而需要注意的是子数组数目,假设此时数组为5 2 6,此次<k,那么以6为右端点的<k的子数组为[5,2,6],[2,6],[6] 故总结若以 right 为端点,则left向右移到<k后,所有的[left ---right],[left+1 ---right]·····[right-1,right],[rigth]都满足条件,即为right-left+1
func numSubarrayProductLessThanK(nums []int, k int) (ans int) {
if k <= 1 {
return
}
prod, left := 1, 0
for right, x := range nums {
prod *= x
for prod >= k { //不满足条件时循环
prod /= nums[left]
left++
}
ans += right - left + 1//在满足条件后取加法
}
return ans
}
练习LeetCode3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
按照套路,for range枚举右指针,cnt[c]记录字符出现次数,cnt[c]>1即子数组内有重复字符时移动左指针,即左指针从不满足条件移动到满足条件
func lengthOfLongestSubstring(s string) (ans int) {
l := 0
cnt:=[128]int{}
for r,c:=range s{
cnt[c]++
for cnt[c]>1{//不满足条件时循环
cnt[s[l]]--
l++
}
ans = max(ans,r-l+1)//在满足条件后取最大值
}
return ans
}
func max(a, b int) int { if b > a { return b }; return a }
练习LeetCode1004. 最大连续 1 的个数
给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。
最多翻转 k 个 0,即数组中最多允许有k个0 按照套路,for range枚举右指针,cnt记录子数组中0的个数,当cnt>target即不满足条件时移动左指针
func longestOnes(nums []int, k int) int {
l,ans,cnt:=0,0,0
for r,num:=range nums{
if num==0{
cnt++
}
for cnt>k{ //不满足条件时循环
if nums[l]==0{
cnt--
}
l++
}
ans=max(ans,r-l+1)//在满足条件后取最大值
}
return ans
}
func max(a, b int) int { if a < b { return b }; return a }
进阶LeetCode1234. 替换子串得到平衡字符串
有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。请返回待替换子串的最小可能长度。如果原字符串自身就是一个平衡字符串,则返回 0。
按照套路,for range 枚举右指针, cnt桶装法记录Q,W,E,R在子串外出现次数,若在子数组外Q,W,E,R出现小于m次,则在子串内一定可以通过替换字符达成平衡字符串,
func balancedString(s string) int {
left,m:=0,len(s)/4
cnt:=[100]int{}
ans:=1000001
for _,c:=range s{
cnt[c]++
}
if cnt['Q'] == m && cnt['W'] == m && cnt['E'] == m && cnt['R'] == m {
return 0 // 已经符合要求啦
}
for right,c:=range s{
cnt[c]--
for cnt['Q'] <= m && cnt['W'] <= m && cnt['E'] <= m && cnt['R'] <= m{//满足条件
ans=min(ans,right-left+1)
cnt[s[left]]++
left++
}
}
return ans
}
func min(a,b int)int{if a>b{ return b}; return a}
进阶LeetCode1658. 将 x 减到 0 的最小操作数
给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。 如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。
法一: 把问题转换成nums 中移除一个最长的子数组,使得剩余元素的和为 x」。
换句话说,要从 nums 中找最长的子数组,其元素和等于 sum-x,这里 sum 为nums 所有元素之和。
最后答案为 nums 的长度减去最长子数组的长度。
func minOperations(nums []int, x int) int {
n:=len(nums)
sum:=0
for _,num:=range nums{
sum+=num
}
if sum<x{
return -1
}else if sum==x{
return n
}
ans:=maxSubArrayLen(sum-x,nums)
if ans==-1{
return ans
}
return n-ans
}
func max(x,y int) int {if x>y{return x}; return y}
//数组中和为target的最长子数组
func maxSubArrayLen(target int, nums []int) int {
ans, s, left := -1, 0, 0
for right, x := range nums {
s += x
for ;s > target;left++ { // 满足要求
s -= nums[left]
}
if s==target{
ans=max(ans,right-left+1)
}
}
if ans>0{
return ans
}
return -1//-1表示无法满足
}
法二:直接双指针
分为前缀与后缀,首先算出最长的元素和不超过 x 的后缀,然后不断枚举前缀长度,另一个指针指向后缀最左元素,答案就是前缀+后缀长度之和的最小值。
func minOperations(nums []int, x int) int {
s, n := 0, len(nums)
right := n
for right > 0 && s+nums[right-1] <= x { // 计算最长后缀
right--
s += nums[right]
}
if right == 0 && s < x { // 全部移除也无法满足要求
return -1
}
ans := n + 1
if s == x {
ans = n - right
}
for left, num := range nums {
s += num
for ; right < n && s > x; right++ { // 不满足条件,缩小后缀长度
s -= nums[right]
}
if s > x { // 缩小失败,说明前缀过长
break
}
if s == x {
ans = min(ans, left+1+n-right) // 前缀+后缀长度
}
}
if ans > n {
return -1
}
return ans
}
func min(a, b int) int { if b < a { return b }; return a }