如何想到这个解法
问题的特点:
- 首先,认识到这是一个关于子串的问题,而且需要考虑子串的最小长度。这提示我们可能需要使用一种方式来逐步探索不同的子串。
- 滑动窗口的适用性:滑动窗口是处理子串问题的常用技巧,特别是当我们需要找到满足特定条件的最短或最长子串时。在这个问题中,我们需要找到包含所有特定字符的最短子串,这正是滑动窗口擅长的。
- 动态调整窗口大小:认识到我们可以动态地调整窗口的大小来探索不同的子串。通过扩展和收缩窗口的边界,我们可以有效地覆盖所有可能的子串,同时保持对窗口内字符的跟踪。
- 字符计数:为了判断窗口内的子串是否包含了 t 中的所有字符,我们可以使用字符计数。通过比较 s 的子串和 t 中字符的计数,我们可以知道当前窗口是否满足条件。
- 优化性能:理解到在移动窗口时,我们不需要每次都从头开始计算。我们可以利用之前的计算结果,每次只对进入和离开窗口的字符进行计数更新。这样,算法的效率更高。
解题思路的构思
- 初始化计数器和窗口指针:首先,计算字符串 t 中每个字符的出现次数,并初始化两个指针(l 和 r)来表示窗口的左右边界。
- 移动右边界以扩展窗口:逐步移动右边界,每次移动都更新窗口内的字符计数。每当窗口包含所有 t 中的字符时,我们就找到了一个潜在的答案。
- 收缩左边界以缩小窗口:一旦窗口满足条件,尝试移动左边界以缩小窗口大小。这一步是为了找到更短的满足条件的子串。
- 更新最短覆盖子串:在每个满足条件的窗口中,如果当前窗口的长度小于之前找到的最短子串,更新最短子串。
- 继续直到结束:重复以上步骤,直到右边界到达字符串 s 的末尾。
class Solution {
public static String minWindow(String s, String t) {
// 检查输入字符串是否有效
if (s == null || t == null || s.length() < t.length()) {
return "";
}
// tCount 用于存储 t 中每个字符出现的次数
int[] tCount = new int[100];
for (int i = 0; i < t.length(); i++) {
tCount[t.charAt(i) - 'A']++;
}
// sCount 用于存储当前窗口中每个字符出现的次数
int[] sCount = new int[100];
// 初始化最小窗口的起始位置和长度
int start = 0, minLen = Integer.MAX_VALUE;
// required 用于跟踪窗口中还需要多少个 t 中的字符
int required = t.length();
// 初始化左右指针
int l = 0, r = 0;
// 遍历 s
while (r < s.length()) {
// 当前字符在 t 中
if (tCount[s.charAt(r) - 'A'] > 0) {
// 更新窗口内字符计数
sCount[s.charAt(r) - 'A']++;
// 如果窗口中的字符数量满足 t 中的需求,则减少 required
if (sCount[s.charAt(r) - 'A'] <= tCount[s.charAt(r) - 'A']) {
required--;
}
}
// 当 required 为 0 时,窗口已包含 t 的所有字符
while (required == 0) {
// 检查当前窗口是否是最小窗口
if (r - l + 1 < minLen) {
minLen = r - l + 1;
start = l;
}
// 尝试移动左边界以缩小窗口
if (tCount[s.charAt(l) - 'A'] > 0) {
sCount[s.charAt(l) - 'A']--;
// 如果移动左边界后窗口不再满足条件,增加 required
if (sCount[s.charAt(l) - 'A'] < tCount[s.charAt(l) - 'A']) {
required++;
}
}
l++;
}
r++;
}
// 返回最小覆盖子串,如果没有找到则返回空字符串
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}