一、题目描述
给你一个字符串 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.length
n == t.length
1 <= m, n <= 10^5
s
和t
由英文字母组成
二、解题思路
- 创建两个哈希表(或数组),分别用来记录字符串
t
中每个字符的出现次数以及当前窗口中每个字符的出现次数。 - 初始化两个指针,
left
和right
,分别表示窗口的左右边界,初始时都指向s
的第一个字符。 - 移动
right
指针扩大窗口,直到窗口中包含了t
中所有的字符。 - 记录下当前窗口的起始位置和长度,这可能是我们要找的最小覆盖子串。
- 移动
left
指针缩小窗口,直到窗口不再包含t
中所有的字符。 - 重复步骤3-5,直到
right
指针到达s
的末尾。 - 在所有满足条件的最小覆盖子串中,返回长度最小的那个。
三、具体代码
import java.util.HashMap;
public class Solution {
public String minWindow(String s, String t) {
if (s == null || s.length() == 0 || t == null || t.length() == 0) {
return "";
}
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
int start = 0, len = Integer.MAX_VALUE;
while (right < s.length()) {
char c = s.charAt(right);
right++;
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
while (valid == need.size()) {
if (right - left < len) {
start = left;
len = right - left;
}
char d = s.charAt(left);
left++;
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 遍历字符串
t
并构建need
哈希表的时间复杂度是O(n),其中n是字符串t
的长度。 - 双指针遍历字符串
s
的时间复杂度是O(m),其中m是字符串s
的长度。 - 在内部循环中,每个字符最多被访问两次(一次是被
right
指针访问,一次是被left
指针访问),因此内部循环的时间复杂度是O(m)。 - 综上所述,总的时间复杂度是O(m + n)。
2. 空间复杂度
need
哈希表存储字符串t
中每个字符的出现次数,空间复杂度是O(n)。window
哈希表存储窗口中每个字符的出现次数,最坏情况下空间复杂度也是O(m)。- 除了哈希表之外,还有几个整型变量用于存储指针位置和计数,它们的空间复杂度是O(1)。
- 因此,总的空间复杂度是O(m + n),主要取决于哈希表的大小。
- 在实际情况中,如果
s
和t
都是由英文字母组成,那么哈希表的大小可以认为是常数级别的,因为英文字母的数量是有限的。 - 在这种情况下,空间复杂度可以近似认为是O(1)。
五、总结知识点
1. 哈希表(HashMap):
- 用于存储字符串
t
中每个字符的出现次数(need
哈希表)。 - 用于存储窗口中每个字符的出现次数(
window
哈希表)。 - 使用
getOrDefault
方法来避免处理不存在键的情况。 - 使用
containsKey
方法来检查一个键是否存在于哈希表中。
2. 字符串操作:
- 使用
toCharArray
方法将字符串转换为字符数组,以便遍历字符串中的每个字符。 - 使用
charAt
方法获取字符串中特定索引处的字符。
3. 双指针技术:
- 使用两个指针
left
和right
来表示滑动窗口的左右边界。 - 通过移动指针来扩展和收缩窗口。
4. 循环和条件语句:
- 使用
while
循环来遍历字符串s
,直到right
指针到达字符串的末尾。 - 使用内部
while
循环来在满足条件时收缩窗口。
5. 变量和赋值:
- 使用变量
valid
来跟踪窗口中满足need
条件的字符数量。 - 使用变量
start
和len
来记录最小覆盖子串的起始位置和长度。
6. 字符串截取:
- 使用
substring
方法来截取最终的最小覆盖子串。
7. 边界检查:
- 在方法开始时检查输入字符串是否为空或长度为零,以避免空指针异常和无效操作。
8. 整数常量:
- 使用
Integer.MAX_VALUE
作为初始的最小覆盖子串长度,以便在找不到覆盖子串时能够识别。
9. 逻辑比较:
- 使用
equals
方法来比较两个整数对象的内容是否相等,而不是比较它们的引用。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。