[算法学习03] 同向双指针的使用


前言

本节是同向双指针的学习。

同向双指针的使用前提,单调性(数组中所有元素的性质一样(正数、字母....))。

先贴上灵神的链接。

同向双指针 滑动窗口【基础算法精讲 03】_哔哩哔哩_bilibili

涉及题目主要有 

leetcode 209、 713、3、1004、1234、1658

一、Leetcode209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

解题思路:使用同向双指针

不满足条件的时候使用右指针扩大窗口

满足条件了以后通过left = left + 1 缩小窗口,目的是找到以right为右端点的长度最短子数组其和大于等于target。

代码的小细节:

  • 题目要求返回的是满足条件的最小子数组的长度,因此初始化ans的时候,可以选择一个最大值,这里使用的float('inf')。
  • 求数组的长度,使用 尾 - 头 + 1

写法一 每次更新答案在嵌套循环里更新

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left, right = 0, 0 
        n = len(nums)
        s = 0
        ans = float('inf')
        while right < n:
            s += nums[right]
            while s >= target:
                ans = min(ans, right - left + 1)
                s = s - nums[left]
                left = left + 1
            right = right + 1 
        return 0 if ans == float('inf') else ans  

写法二 将ans的更新从循环中拿了出来

那么在循环结束的时候就应该加上一句 if  s >= target : ans = min(ans, right - left + 1) 

只有在满足条件的情况下,才更新ans

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left = 0
        s = 0
        ans = float('inf')
        for right, x in enumerate(nums):
            s += x
            while s - nums[left] >= target:
                s -= nums[left]
                left = left + 1
            if s >= target:
                ans = min(ans, right - left + 1)
        return 0 if ans == float('inf') else ans 

二、Leetcode713. 乘积小于 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。

示例 1:

输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

示例 2:

输入:nums = [1,2,3], k = 0
输出:0

提示: 

  • 1 <= nums.length <= 3 * 104
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 106

解题思路 使用同向双指针,右指针扩大窗口,左指针缩小窗口。

基本和上一题无区别,需要注意的就是

当子数组的乘积严格 < k 时,那么在这个子数组里一共有right - left + 1 个元素 < k,不包括左端点。

[5] k = 50 ,对于这个子数组来说 有[5] 一个子数组严格小于50。 right - left + 1 = 1

[5,8] k = 50 , 对于这个子数组来说 有[5, 8]、[8] 两个子数组严格小于50。  right - left + 1 = 2

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        left = 0
        m = 1
        ans = 0
        for right, x in enumerate(nums):
            m *= x 
            while left <= right and m >= k:
                m /= nums[left]
                left = left + 1
            ans += right - left + 1
        return ans   

还有要补充的一点就是, 对于特殊的k = 0, 我们需要在子循环中 加入条件 left <= right 才能特殊处理掉。 因为每次子循环结束,我们都会更新ans的值, 但对于特殊的 k = 0,只有当 left = right + 1的时候,才能保证 ans += 0。

因为数组的元素严格大于1,故不可能存在乘积 < 1的子数组。

当然也可以在 开头之间判断 if k<= 1 return 0

三、Leetcode3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

解题思路 

都是套路题,和之前两题没太多区别。

这题注意就是 用set()存储字符串,可以提高搜索效率 达到O(1)。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        left = 0
        subStr = set()
        ans = 0 
        for right, x in enumerate(s):
            while x in subStr:
                subStr.remove(s[left])
                left += 1
            subStr.add(x)
            ans = max(ans, right - left + 1)
        return ans

四、Leetcode1004. 最大连续 1 的个数 III

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

提示:

  • 1 <= nums.length <= 105
  • nums[i] 不是 0 就是 1
  • 0 <= k <= nums.length

解题思路

题目要求得到最长连续1的子数组长度,其中可以将k个0变为1。

我们可以设置一个 计数变量cnt 用来统计 到目前为止我们将多少个0变成 1了。

如果 cnt > k 说明当前的子数组已经不在满足条件,我们需要移动左指针left缩小滑动窗口,同时减小cnt,使得 cnt <= k。

class Solution:
    def longestOnes(self, nums: List[int], k: int) -> int:
        left = 0
        ans = 0
        cnt = 0
        for right, x in enumerate(nums):
            if x == 0:
                cnt += 1
            while cnt > k:
                if nums[left] == 0:
                    cnt -= 1
                left = left + 1
            ans = max(ans, right - left + 1)
        return ans 

五、leetcode 1234. 替换子串得到平衡字符串

有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。

假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。

给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。

你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。

请返回待替换子串的最小可能长度。

如果原字符串自身就是一个平衡字符串,则返回 0

示例 1:

输入:s = "QWER"
输出:0
解释:s 已经是平衡的了。

示例 2:

输入:s = "QQWE"
输出:1
解释:我们需要把一个 'Q' 替换成 'R',这样得到的 "RQWE" (或 "QRWE") 是平衡的。

示例 3:

输入:s = "QQQW"
输出:2
解释:我们可以把前面的 "QQ" 替换成 "ER"。 

示例 4:

输入:s = "QQQQ"
输出:3
解释:我们可以替换后 3 个 'Q',使 s = "QWER"。

解题思路

题目要求:替换一个子串使得主串中各字符出现次数平均

材料: 平均出现次数 len(s) // 4   主串中每个字符出现次数 cnt = Counter(s) 

做法:

步骤1: if all( cnt [c] == m for c in "QWER"): return 0

如果主串中每个字符的出现次数均为m,则说明原本就是平均的,返回0。

步骤2:

同向双指针滑动窗口

right + 1

cnt[right] -= 1

上述操作的目的 是 使得 if all(cnt[c] <= m for c in "QWER") 

只有当 主串中每个字符的出现次数都小于等于 m 

这个时候, 我们可以利用子串去把主串变得平均, 字串的长度 就是 right - left + 1

当条件满足, 我们就可以 缩小子串 逐步求最小子串的长度。

以下是代码

class Solution:
    def balancedString(self, s: str) -> int:
        cnt, m = Counter(s), len(s) // 4
        if all(cnt[c] == m for c in "QWER"):
            return 0
        ans, left = inf, 0
        for right, x in enumerate(s):
            cnt[x] -= 1
            while all(cnt[c] <= m for c in "QWER"):
                ans = min(ans, right - left + 1)
                cnt[s[left]] += 1
                left = left + 1
        return ans 


​六、 Leetcode 1658. 将 x 减到 0 的最小操作数

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。

如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。

示例 1:

输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。

示例 2:

输入:nums = [5,6,7,8,9], x = 4
输出:-1

示例 3:

输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 104
  • 1 <= x <= 109

解题思路:

题目要求我们每次用X 减去 数组最左边 or 最右边的数,直到x=0。

正难则反!  

我们可以在数组中间 找到一个子数组 让这个子数组的和 等于 Sum(nums) - x, 并且这个子数组的长度最大! 这样len(nums) - 子数组长度 就是我们的答案了。

值得注意的是:

target = Sum(nums) - x 

如果 target < 0, 说明 不管怎么做 我们都不能使这个 x 减到0 故返回 -1

以下是代码

class Solution:
    def minOperations(self, nums: List[int], x: int) -> int:
        target = sum(nums) - x
        if target < 0:
            return -1
        left = s = 0
        answer = -1
        for right, n in enumerate(nums):
            s += n
            while s > target:
                s -= nums[left] 
                left = left + 1
            if s == target:
                answer = max(answer, right - left + 1)
        return answer if answer < 0 else len(nums) - answer

七、LeetCode 2962. 统计最大元素出现至少 K 次的子数组 

给你一个整数数组 nums 和一个 正整数 k 。

请你统计有多少满足 「 nums 中的 最大 元素」至少出现 k 次的子数组,并返回满足这一条件的子数组的数目。

子数组是数组中的一个连续元素序列。

示例 1:

输入:nums = [1,3,2,3,3], k = 2
输出:6
解释:包含元素 3 至少 2 次的子数组为:[1,3,2,3]、[1,3,2,3,3]、[3,2,3]、[3,2,3,3]、[2,3,3] 和 [3,3] 。

示例 2:

输入:nums = [1,4,2,1], k = 3
输出:0
解释:没有子数组包含元素 4 至少 3 次。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 106
  • 1 <= k <= 105

解题思路:

首先 target = max(nums) 找到最大元素

接着构造窗口,滑动

left = ans = 0

for right, x in enumerate(nums):

        if x == target:

                k -= 1   target 出现就让k-1

        while k == 0:  此时已经找到了一个出现次数 == k 的数组,缩小窗口

                if nums[left] == target:

                        k += 1

                left += 1

        ans += left  上面循环结束时,说明[left-1, right] 是满足条件的最小子数组

[left-1, right](枚举的 x = nums[right])区间出现的最大元素个数出现次数「恰好为 k 次」。那么 [left1, right],left1 ∈ [0, left-1] 都是符合题意的区间,有 left 个

下面是代码

class Solution:
    def countSubarrays(self, nums: List[int], k: int) -> int:
        target = max(nums)
        left = 0
        ans = 0
        for right, x in enumerate(nums):
            if x == target:
                k = k - 1
            while k == 0:
                if nums[left] == target:
                    k += 1
                left = left + 1
            ans += left
        return ans 
            



总结

继续加油!!

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值