76. 最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:如果 s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例 2:
输入:s = "a", t = "a"
输出:"a"
提示:
1 <= s.length, t.length <= 105
s
和t
由英文字母组成
进阶:你能设计一个在 o(n)
时间内解决此问题的算法吗?
解体思路
方法一: 滑动窗口
思路很容易想到。首先设置两个指针left, right, 分别代表滑动窗口的左右边界, 初始时指向为0。首先先让右边界逐渐向右移动, 直到滑动窗口内部已经包含了T中的所有字符。
在滑动窗口内部已经包含了T中所有字符过后, 让左边界逐渐向右移动, 尽可能得缩小滑动滑动大小。最小滑动窗口的大小总是在移动左边界的过程中得出。
在恰好不能包含T中所有字符时, 可以对最小长度进行更新。并且记录下此时窗口的起始位置和长度。
在判断滑动窗口内字符是否包含了T中所有字符时, 有一个小小的技巧。
设置一个变量distance表示窗口内字符里T中的字符出现的个数。对T中某个字符, 窗口内字符出现的个数最大为T中此字符个数。
比如窗口字符为aaabbbcccc
, 而T字符为aabbc
, 那么对于窗口的字符统计为winFreq['a'] = 3, winFreq['b'] = 3, winFreq['c'] = 3
, 距离distance = 3
。
这样, distance最大为T字符串的长度。当distance == tLen
时, 代表此时滑动窗口已经包含了所有字符(但是winFreq内的字符统计, 可能会比T串字符出现的个数多, 这是允许的)。
同理在左边界逐渐向右移动, 恰好不包含T中字符时, 也是同样的方法, 当某个字符窗口个数和T串个数恰好相等时, 如果左边界继续右移, 则窗口将不再包含T中所有字符。此时需要将distance--。
public String minWindow(String s, String t) {
int sLen = s.length(), tLen = t.length();
char[] ss = s.toCharArray();
char[] tt = t.toCharArray();
// winFreq, tFreq分别用于统计滑动窗口内部和t串内字符出现次数
int[] tFreq = new int[128];
int[] winFreq = new int[128];
for (char c : tt) {
tFreq[c]++;
}
int left = 0, right = 0;
// distance表示在s字符串中,t中字符出现的个数
// 对单个字符而言, 最多出现t中字符串出现的次数
int distance = 0;
// 最小子串的长度
int minLen = Integer.MAX_VALUE;
int resBegin = 0;
while (right < sLen) {
if (tFreq[ss[right]] > 0) {
// 如果右边界字符是t中出现的字符
if (winFreq[ss[right]] < tFreq[ss[right]]) {
// 计数距离自增
distance++;
}
// 滑动窗口内对此字符计数
winFreq[ss[right]]++;
while (distance == tLen) {
// distance == tLen 表示滑动窗口当中已经包含了全部T中的字符
// 此时需要左窗口不断向右滑动
if (tFreq[ss[left]] > 0) {
// 左边界的字符在滑动窗口出现,
if (winFreq[ss[left]] == tFreq[ss[left]]) {
if (minLen > right - left + 1) {
// 需要更新长度
minLen = right - left + 1;
resBegin = left;
}
distance--;
}
// 修改次数
winFreq[ss[left]]--;
}
left++;
}
}
right++;
}
if (minLen == Integer.MAX_VALUE) {
// S中没有包含T的最小覆盖子串
return "";
}
return s.substring(resBegin, resBegin + minLen);
}