最小覆盖子串
题目描述
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。
示例:
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:
- 如果 S 中不存这样的子串,则返回空字符串
""
。 - 如果 S 中存在这样的子串,我们保证它是唯一的答案。
解题思路
个人AC
理解有偏差,没有AC。单纯地以为T中的字母不会有重复,只需要用HashMap
来记录每个字符出现的位置即可。
class Solution {
public String minWindow(String s, String t) {
HashMap<Character, Integer> window = new HashMap() {{
for (int i = 0; i < t.length(); i++) {
this.put(t.charAt(i), -1);
}
}};
HashMap<Character, Integer> target = new HashMap() {{
for (int i = 0; i < t.length(); i++) {
this.put(t.charAt(i), -1);
}
}};
int minSum = Integer.MAX_VALUE;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!window.containsKey(c)) continue;
window.put(c, i);
int sum = calDiffAbsSum(window);
if (sum < 0) continue;
if (sum < minSum) {
for (Character key : window.keySet()) {
target.put((key), window.get(key));
}
}
}
int start = Integer.MAX_VALUE, end = Integer.MIN_VALUE;
for (int v : target.values()) {
if (v < 0) return "";
if (v < start) start = v;
if (v > end) end = v;
}
return s.substring(start, end + 1);
}
// 计算两两差值绝对值的和
private int calDiffAbsSum(HashMap<Character, Integer> window) {
int sum = 0;
for (int a : window.values()) {
if (a < 0) return -1;
for (int b : window.values()) {
if (b < 0) return -1;
if (a != b) {
sum += Math.abs(a - b);
}
}
}
return sum / 2;
}
}
执行结果:解答错误
输入:
"a"
"aa"
输出:
"a"
预期结果:
""
最优解
滑动窗口
思路:
- 在字符串
s
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引区间[left, right]
称为一个窗口; - 先不断地向右移动
right
指针,扩大窗口,直到窗口中的字符串符合要求(包含了t
中的所有字符); - 此时,停止移动
right
,转而不断移动left
指针缩小窗口,直到窗口中的字符串不再符合要求(不包含t
中的所有字符),每次增加left
时,都要更新一轮内容; - 重复第
2
和3
步,直到right
到达字符串s
的末尾。
本质:第2
步相当于寻找一个“可行解”,第3
步相当于优化这个“可行解“,最终找到最优解。
needs
和window
相当于计数器,分别记录t
中字符出现次数和窗口中的相应字符的出现次数。
初始状态:
移动right
,直到窗口[left, right]
包含了t
中所有字符:
然后移动left
,缩小窗口[left, right]
:
直到窗口中的字符串不再符合要求,left
不再继续移动:
之后重复上述过程,先移动right
,再移动left
…… 直到right
指针到达字符串s
的末尾,算法结束。
上述过程可以简单地写出如下伪码框架:
string s, t;
// 在 s 中寻找 t 的「最小覆盖子串」
int left = 0, right = 0;
string res = s;
while(right < s.size()) {
window.add(s[right]);
right++;
// 如果符合要求,移动 left 缩小窗口
while (window 符合要求) {
// 如果这个窗口的子串更短,则更新 res
res = minLen(res, window);
window.remove(s[left]);
left++;
}
}
return res;
如何判断window
即子串s[left:right+1]
是否包含t
的所有字符呢?
可以用两个哈希表当作计数器解决。用一个哈希表needs
记录字符串t
中包含的字符及出现次数,用另一个哈希表window
记录当前窗口[left, right]
中包含的字符及出现的次数,如果window
包含所有needs
中的键,且这些键对应的值都大于等于needs
中的值,那么就可以知道当前窗口[left, right]
符合要求了,然后可以移动left
指针了。
class Solution {
public String minWindow(String s, String t) {
// 记录最短子串的开始位置和长度,即覆盖最小子串的窗口
int minLeft = 0, minLen = Integer.MAX_VALUE;
// 当前窗口
int left = 0, right = 0;
HashMap<Character, Integer> needs = new HashMap<Character, Integer>() {{
for (int i = 0; i < t.length(); i++) {
char key = t.charAt(i);
this.put(key, this.getOrDefault(key, 0) + 1);
}
}};
HashMap<Character, Integer> window = new HashMap<>();
int match = 0;
while (right < s.length()) {
char c = s.charAt(right);
if (needs.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
// 常量池 -128 ~ 127,若不手动拆箱,当字符串过长时会出现问题
if (window.get(c).intValue() == needs.get(c).intValue()) {
match++;
}
}
right++;
// 不断移动right指针,直到找到包含t中所有字母的子串
// 然后移动左指针,优化当前解
while (match == needs.size()) {
if (right - left < minLen) {
// 更新最小子串的开始位置和长度
minLeft = left;
minLen = right - left;
}
c = s.charAt(left);
if (needs.containsKey(c)) {
window.put(c, window.get(c) - 1);
if (window.get(c) < needs.get(c)) {
// 字符c的出现次数不再符合要求
match--;
}
}
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
}
}
时间复杂度:
O
(
m
+
n
)
O(m + n)
O(m+n),其中m
和n
分别是字符串s
和t
的长度;
空间复杂度: O ( m ) O(m) O(m);