算法套路一:同向双指针

141 阅读7分钟

同向双指针——滑动窗口

套路讲解实例一:LeetCode209. 数组和 ≥ target 的最子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 在这里插入图片描述

  1. 暴力双循环 时间复杂度O(n^2).
  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 }