前言
接上一篇的双索引问题
1、给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:
如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
思路1:
碰到这种子序列子数组子字符串的问题,可以想想滑动窗口的用法,我们从左向右扫描,时间复杂度为O(n),缩小窗口的循环几乎考虑,所以并没有n^2的复杂度
情况1:当区间的和小于s的时候,就继续向右移动,求和,直到和大于等于s的时候,我们再来一层循环
情况2:在内存循环中,我们的目的是尝试去缩小区间,先记录下区间长度,取已有长度和现在长度更小的,然后缩小区间,当这一层出来时,又可以继续向后遍历了,最终输出最小的区间长度
根据思路写代码就很容易了
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if len(nums) == 0:
return 0
l = 0 #用来尝试缩小区间的
cur_sum = 0
res = len(nums) + 1
for i in range(len(nums)):#右指针就是用来遍历的指针
cur_sum += nums[i]
while cur_sum >= s:#遍历找到了和大于s时候
res = min(res,i - l + 1) #获取长度
#减去左边的值,尝试缩小长度
cur_sum -= nums[l]
l += 1
return 0 if res == len(nums) + 1 else res
还有一种二分法的思路,没有这种解法好,感兴趣的可以了解一下,我就不废脑细胞了
思路2:二分法,利用“数组是正整数”这个条件,构造前缀和数组,这个前缀和数组一定是严格增加的。 任意区间和可以通过前缀和数组得到,这是我们常见的一种做法。 起点固定的时候,区间越长,区间和越大。
public class Solution {
public int minSubArrayLen(int s, int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
// 构造前缀和数组
// 因为 nums 全都是正整数,因此 preSum 严格单调增加
int[] preSum = new int[len];
preSum[0] = nums[0];
for (int i = 1; i < len; i++) {
preSum[i] = preSum[i - 1] + nums[i];
}
// 因为前缀和数组严格单调增加,因此我们可以使用二分查找算法
// 最后一位没有下一位了,所以外层遍历到最后一位的前一位就可以了
int ret = len + 1;
for (int i = 0; i < len - 1; i++) {
// 计算区间和
int l = i;
int r = len - 1;
// 设置成一个比较大的数,但是这个数有下界
// i 的最大值是 len - 2,
// ans - i + 1 >= len + 1
// ans >= i + len = 2 * len -2
int ans = 2 * len - 2;
// int ans = 2 * len - 1; 能通过
// int ans = 2 * len - 3; 不能通过
// 退出循环的条件是 l > r
while (l <= r) {
int mid = l + (r - l) / 2;
// 计算一下区间和,找一个位置,使得这个位置到索引 i 的区间和为 s
// 13 14 15 17 19 20
int segmentSum = preSum[mid] - (i == 0 ? 0 : preSum[i - 1]);
if (segmentSum >= s) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
ret = Integer.min(ans - i + 1, ret);
}
if (ret == len + 1) {
return 0;
}
return ret;
}
}
2、给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:
输入:
s: “cbaebabacd” p: “abc”
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
示例 2:
输入:
s: “abab” p: “ab”
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。
思路:
用一个map去保存p中出现的字符计数,然后用滑窗遍历s,当右边界滑过时,就让对应的计数减1,而左边滑过就需要让计数加1,设置一个distance表示子串的长度,初始和p相等,然后,当滑窗滑动时,右边的字符如果在就让distance-1,如果左边的游标移动,就让distance+1,当长度为0时,就表示子串长度刚好为p的,而且,字符也满足正好是p中字符的条件。
这题的思想还是很难理解的:
1、用distance记录长度,限定了子串的长度,distance只有在子串的字符还在hash计数中有的时候才会减1,这保证了子串的长度刚好为p的长度时,子串的字符计数和p完全一致;
2、hash计数在什么时候变化呢?当右边滑窗边界增加之前,先判断是否这个字符是出现在p中的去改变distance,然后让hash计数减1证明,已经用distance计数了
3、判断distance是否满足,满足就记录下左边游标,注意前半段会把滑窗给撕开,长度总是会在plen和plen+1之间变化,一旦为plen+1,就去调整左边,左边界的那个字符如果出现在p中,这时,它的计数肯定是大于等于0的,因为被右滑窗减过,所以要移出它,也得记得把distance恢复一,然后在移出这个字符,让hash中计数加1。
总结这个滑窗思想就是利用了两个游标此消彼长的特点,然后处理好细节就行了,有点难理解
def findAnagrams(self, s: str, p: str) -> List[int]:
from collections import defaultdict
dict_hash = defaultdict(int)
size = len(s)
l = 0
r = 0
res = []
#用hash记录p中字符的出现个数
for c in p:
dict_hash[c] += 1
plen = len(p)
distance = plen #用来统计长度
while r < size:
#右边界增加
if dict_hash[s[r]] > 0:
#当前字符出现在hash中,让距离减小1
distance -= 1
#让对应的s[r]减去1,本来不在就为负数了,本来在的就减1,减去该字符的一个计数
dict_hash[s[r]] -= 1
r += 1
#证明找到了一个子串
if distance == 0:
res.append(l)
if r - l == plen: #当左右边之间差了一个plen的时候,需要调整左边了,子串长度已经超过了p
if dict_hash[s[l]] >= 0:#如果左边对应的值在p中,因为这个时候,hash值已经减过一次了
distance += 1 #让ditance加回1
#恢复该字符的一个计数
dict_hash[s[l]] += 1
l += 1
return res
思路二:
这种思路比较清晰,就是先给p中的字符做一个频数统计,然后遍历s的时候,统计子串的字符频数,一致的话就满足条件
def findAnagrams(self, s: str, p: str) -> List[int]:
res = []
slen = len(s)
plen = len(p)
scnt = [0]*26
pcnt = [0]*26
for c in p:
pcnt[ord(c) - ord('a')] += 1
for end in range(slen):
if end >= plen:
scnt[ord(s[end - plen]) - ord('a')] -= 1
scnt[ord(s[end]) - ord('a')] += 1
if scnt == pcnt:
res.append(end - plen + 1)
return res
3、给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。
示例:
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:
如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
思路:这题是比较难的
1、先用一个map去统计t中的字符个数,
2、然后使用两个游标,l,r,先去找s中包含t中所有元素的子串,用另一个map,每出现一个字符在t中就让这个新map中的值加1,然后当两个map中对应的字符计数相等时,代表这个字符满足了,用一个计数变量加以记录,当计数变量和需要的相等时,就证明,当前滑窗之中已经包含了t中所有的字符。
3、然后进行内部循环,尝试去缩短左边界,先更新结果字符串,选取较短的那个更新,然后如果左边界的字符是t中的话,就把我们新map对应的值减1,并且满足的计数变量也需要减1,否则就缩小左游标
4、重复第二步直到遍历完整个字符串
def minWindow(s, t) -> str:
from collections import defaultdict
dict_need = defaultdict(int) # 模板的map
dict_window = defaultdict(int) # 统计子串的map
l = r = 0 # 左右游标
size = len(s)
match_len = 0 # 匹配的len
for c in t:
dict_need[c] += 1
need_len = len(dict_need) # 需要的len
res = ''
flag = True # 判断是否是第一次满足匹配
while r < size:
if dict_need[s[r]] > 0:
dict_window[s[r]] += 1
if dict_need[s[r]] == dict_window[s[r]]:
# 当一个字符完全满足时就让满足的加1
match_len += 1
r += 1
while match_len == need_len:
if flag:
res = s[l:r]
else:
res = res if len(res) < len(s[l:r]) else s[l:r] # 取更小长度的子串
# 开始左移
# 要是左边界字符是t中的,需要将window中的减1,此时match也要减1
if dict_need[s[l]] > 0:
dict_window[s[l]] -= 1
if dict_window[s[l]] < dict_need[s[l]]: #只有window中的值比需要的小的时候才把满足的减1
match_len -= 1
l += 1
return res
总结:
滑窗思想在解决字符串子串问题中还是很好用的,虽然加上了一些东西以后,比较困难,多练习,多思考就可以解决的。
愿每一个程序员都能被温柔相待