继续每日一题,今天给大家继续分享一道滑动窗口的题目
题目稍微难度稍微有一点,但是我会带着大家慢慢分析
题目描述:
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
题目示例:
首先先明确一个点:什么叫子串?
子串的含义是:有A B两个字符串,如果A中对于B的每个字符出现的频次和B相等,就可判定B是A的子串
如:A=“ABCDAA” B=“DBC” B是A的子串
接下来我们再看一下滑动窗口的常用套路:
- 定义left,right指针用于维护窗口大小
- 移动右指针扩大窗口范围来匹配我们需要的信息
- 匹配到信息后,执行响应的逻辑,移动左指针缩小窗口,以便让窗口向右继续滑动
注意:所有类型的滑动窗口类型的题目均可以按照这个套路去做
什么时候算是匹配成功,本题的匹配条件是找到子串,即找到窗口内对应的字符串,使的t是窗口的子串,结合上面给大家对子串的定义,也就是转化为找字符出现的频次
此处有一个技巧,针对所有子串相关的频次统计问题,均可以利用数组来对比,但本题稍微有些特殊的点在于两个字符串的字符没有限制大小写,所以我们可以定义一个size=128的数组(ASCII 128 足够覆盖常见字符)来统计每个字符出现的频次
int[] need = new int[128];
int[] window = new int[128];
其中:
- need是需要我们匹配上的,即题目中给的t字符串中每个字符出现的次数
- window是窗口内每个字符出现的次数
什么时候才能算匹配上呢?文字说明不好理解,我列举一个case:
s=“AABCCD” t=“AABCD”
need=[0,0,…,2,1,1,1,0,0…,0]
当index=1时,A的数量=2,和need里A的数量相等,则认为匹配上了1个字符
所以我们在匹配之前,还需要找到需要匹配几个字符,这个也很简单,我们只需要遍历need,找到有多少不为0的位置(类似布隆过滤器)便找到了几个需要匹配的字符
以题目中的case为例:
s = “ADOBECODEBANC”, t = “ABC”
need=[0,0,…,1,1,1,0,0…,0]
有三个不为0的位置,所以待匹配的字符数量totalChar=3
当匹配上的次数matchs==totalChar时,我们需要计算是否是最小子串,并移动left,在移动left的过程中,要保证窗口仍然包含t的所有字符,直到不满足时再继续移动right。
里面有几个核心的代码:
- 声明匹配的数组
int[] need = new int[128];
int[] window = new int[128];
for (Character c : t.toCharArray()) {
need[c]++;
}
- 查找需要匹配的字符数量
int totalChars = 0;
for (int n : need) {
if (n > 0) {
totalChars++;
}
}
- 移动右指针,并计算匹配上的次数
if (right < s.length()) {
Character rightC = s.charAt(right);
if (need[rightC] > 0) {
window[rightC]++;
if (window[rightC] == need[rightC]) {
matchs++;
}
}
}
right++;
- 匹配成功,执行最小子串查找逻辑
while (matchs == totalChars) {
Character leftC = s.charAt(left);
if (right - left < minLength) {
minLength = right - left;
ans = s.substring(left, right);
}
if (need[leftC] > 0) {
if (window[leftC] == need[leftC]) {
matchs--;
}
window[leftC]--;
}
left++;
}
当然很多同学可能对上面这段代码里的
if (need[leftC] > 0) {
if (window[leftC] == need[leftC]) {
matchs--;
}
window[leftC]--;
}
有些许不理解,我同样用上面的case帮大家理解一下:
s = “ADOBECODEBANC”, t = “ABC”
need=[0,0,…,1,1,1,0,0…,0]
在遍历的过程中,B可能出现多次,window=[0,0,…,1,2,1,0,0…,0]
移动指针后,虽然window[c]–:window=[0,0,…,1,1,1,0,0…,0]
但是仍然包含B这个字符,所以matchs不能减一,只有在当前从窗口内,该字符出现的数量恰好和待匹配字符数量相等的时候,一旦移动指针,就少了该字符的匹配,所以才能matchs减一
整体如下:
public static String minWindow(String s, String t) {
int[] need = new int[128];
int[] window = new int[128];
for (Character c : t.toCharArray()) {
need[c]++;
}
int left = 0, right = 0;
int matchs = 0;
int minLength = Integer.MAX_VALUE;
String ans = "";
int totalChars = 0;
for (int n : need) {
if (n > 0) {
totalChars++;
}
}
while (right <= s.length()) {
while (matchs == totalChars) {
Character leftC = s.charAt(left);
if (right - left < minLength) {
minLength = right - left;
ans = s.substring(left, right);
}
if (need[leftC] > 0) {
if (window[leftC] == need[leftC]) {
matchs--;
}
window[leftC]--;
}
left++;
}
if (right < s.length()) {
Character rightC = s.charAt(right);
if (need[rightC] > 0) {
window[rightC]++;
if (window[rightC] == need[rightC]) {
matchs++;
}
}
}
right++;
}
return ans;
}