解题随记
题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
解释: 最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入: s = "a", t = "a"
输出: "a"
解释: 整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s和t由英文字母组成
思路
抓重点
- 如何保障target的所有字符都在窗口内。
- 重复字符的判断,比如BBAC
- 多个满足条件的字符串,如果判断最短
解题方法
- 通过一个数据结构记录目标字符串target中字符对应出现次数(可以用map或者数组),目的是用来
判断当前字符串是否包含target中全部字符 - 遍历源字符串originStr
- 如果当前字符存在于目标字符串target当中,则相应记录数-1,并记录该字段在源字符串originStr中的下标索引位置(不存在则不处理
如果判断map中所有key对应value为0不好控制的话,可以使用target.length判断,注意当出现连续重复的只需要减一次即可) - 当出现满足目标字符串target的子串时,记录head和last(源数据位置);
- 将窗口向
右移一位,并重复判断当前字符串能否覆盖目标字符串target,如果不能则右指针往右移一位,如果能则判断当前窗口字符串是否比上次窗口字符串的长度更短,是则覆盖上次记录,否则跳过。
附代码:
public static String minWindow(String s, String t) {
// 特判,当s长度小于t时,不可能存在改情况数据,返回空串
if (s.length() < t.length()) {
return "";
}
// 得到目标字符串每个字段的出现次数(此处使用两倍空间,主要为了使用后半段判断是否存在,前半段控制加减)
int[] ints = new int[256];
for (int i = 0; i < t.length(); i++) {
ints[t.charAt(i)] ++;
ints[128 + t.charAt(i)] ++;
}
// 结果:s对应下标索引,0-head|1-last
int[] result = new int[2];
int sLength = s.length();
// 用来判断是否覆盖
int tLength = t.length();
// 用来根据索引获取原来字符串对应位置的char值
char[] sCharArray = s.toCharArray();
// 存放原字符串中存在于目标字符串的char下标,通过出队入队获取到最终的head和last值
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
// 当前值
Character c = s.charAt(i);
// 存在于目标字符串中
if (ints[128 + c] > 0) {
// 加入到deque末尾,如果加入到队首,会导致后续出队无效
deque.addLast(i);
// 用完一个减掉一个(这里可能有负数,类似BBBAC)
ints[c] --;
// 因为上面有负数,所以只有正常情况(负数都是重复出现)才能算作有效字段
if (ints[c] >= 0) {
tLength--;
}
}
// 代表出现覆盖子串,t中所有char都出现了
if (tLength == 0) {
// 处理map中负数,也就是类似BBBAC这样的重复数
while (!deque.isEmpty() && ints[sCharArray[deque.peekFirst()]] < 0) {
Integer currentDequeFirstIndex = deque.removeFirst();
ints[sCharArray[currentDequeFirstIndex]] ++;
}
// 到现在deque中就没有重复的char了,我们就要开始计算head和last了
// 我们是按顺序遍历的,所有队首就是头,最小的起始值
int head = deque.removeFirst();
// 控制只有小的才会重新计算head和last,比如当前计算的sLength大于上一次的值,则忽略
if (i - head + 1 <= sLength) {
sLength = i - head + 1;
result[0] = head;
// i+1是为了后续subString好处理点,也可以选择此处不加1,后续结果的的时候再+1
result[1] = i + 1;
}
// 有值出队了,自然空的字符数要加上,相应的判断覆盖条件的也要+1,保持平衡
ints[sCharArray[head]] ++;
tLength++;
}
}
// 按照head和last截取即可
return s.substring(result[0], result[1]);
}
Tips:
- 使用Map + targetLength做加减法判断当前窗口字符串是否包含target中全部字符,当targetLength==0即全部覆盖了。
- 使用Deque按顺序从小到大记录target中字符在源字符串中出现的位置下标,便于滑动窗口。
- 考虑BBCA这种场景,Map中对应字符可能出现负数,所以当全部覆盖场景下,需要先处理下前缀连续重复的数据,连续重复字符保留一个即可。
- 发现满足条件的子串后,需要比较当前窗口的子串长度和之前子串长度,保留较短的那个.
- 因为前面已经处理了连续重复的数据,当队首的字符(符合条件)出队以后,当前窗口字符串一定是不满足所有target字符都在其中,然后遍历的过程就相当于窗口不停向右扩展的一个过程;当再次碰到符合条件的字符串后,比较字符串head和last之前的长度,再次将队首出队,模拟窗口向左收缩的过程.