前言
本节是同向双指针的学习。
同向双指针的使用前提,单调性(数组中所有元素的性质一样(正数、字母....))。
先贴上灵神的链接。
同向双指针 滑动窗口【基础算法精讲 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
那么在循环结束的时候就应该加上一句 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
总结
继续加油!!