解题思路:
- 哈希表存储字符频率:首先统计字符串
t
中每个字符出现的次数。 - 滑动窗口:用两个指针
left
和right
来标记当前窗口的左右边界,不断右移right
,直到包含了所有t
中的字符。然后尝试右移left
,缩小窗口以找到最小覆盖子串。 - 更新最小长度:在窗口满足条件时,记录当前窗口的长度并更新最小长度。
- 时间复杂度:由于左右指针各自只遍历一次字符串,因此时间复杂度为 O(n)。
定义一个变量 count
,用来记录当前窗口中还需要匹配多少个字符。初始化时,count
等于 t
的长度。
一旦 count == 0
,意味着当前窗口已经包含了 t
中所有字符。此时我们尝试通过右移 left 指针来缩小窗口,并检查最小长度。
HashMap<Character, Integer>
的值是动态变化的。它的值在滑动窗口移动时会随着窗口内字符的增减而调整。具体来说,HashMap 的值记录了当前滑动窗口中对剩余字符需要匹配的次数。
Java 代码:
class Solution {
public String minWindow(String s, String t) {
//如果m,n可以取整为0,需要对特殊情况返回空串
if(s.length() == 0 || s == null || t.length() == 0 || t == null) {
return "";
}
//然后,首先统计 t 中字符频率
HashMap<Character, Integer> TCharfrequency = new HashMap<>();
for(char c : t.toCharArray()){
TCharfrequency.put(c, TCharfrequency.getOrDefault(c, 0) + 1);
}
//初始化滑动窗口的左右指针,初始化最小覆盖子串的长度
int left = 0;
int right = 0;
int minLeft = 0;
int minLen = Integer.MAX_VALUE;
//初始化仍然需要匹配的字符次数 count
int count = t.length();
//滑动窗口,HashMap 的value值记录了此时滑动窗口中对应key字符剩余需要匹配的次数,是动态变化的
while(right < s.length()) {
//首先处理当前右指针指向的字符
char rChar = s.charAt(right);
if(TCharfrequency.containsKey(rChar)) {
//如果当前右指针指向的字符在map中出现,说明成功匹配了一个字符,需要减少count
if(TCharfrequency.get(rChar) > 0) {
//之所以需要判断value大于0,是因为如果value==0,说明当前字符匹配次数达到了,不需要再匹配了,
count--;
}
//同时我们需要更新hashmap中对应的value
TCharfrequency.put(rChar, TCharfrequency.get(rChar) - 1);//为什么需要这行代码?
}
//处理完当前右指针字符后滑动窗口, 移动右指针
right++;
//每当滑动窗口之后, 立马进行优化缩小窗口尝试
while(count == 0) { //如果count为0,说明当前窗口已经包含了所有t中的字符
// 如果使用 if,只能尝试缩小一次,也就是说只能缩小一个字符的范围。
// 使用 while 可以确保我们在当前窗口仍然满足所有条件的情况下,反复缩小左边界,直到窗口再也不能缩小为止。
//更新滑动窗口
if(right - left < minLen) {
minLeft = left;
minLen = right - left;
}
// 在更新了窗口之后,尝试缩小窗口
char lChar = s.charAt(left);
if(TCharfrequency.containsKey(lChar)) {
//存在可能性使得右边界右移的步数小于左边界右移的步数同时保持覆盖子串,
//所以即使lChar 在 HashMap 中存在,仍然要移动左指针,更新hash表字符频率
TCharfrequency.put(lChar, TCharfrequency.get(lChar) + 1);
//如果hash表中包含左指针的字符,那么我们仍然移动右移左指针
if(TCharfrequency.get(lChar) > 0) {
count++;//更新待匹配次数计数器
}
}
left++;
}
}
//substring 左闭右开
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
}
}
为什么即使 lChar 在 HashMap 中存在,仍然要移动左指针?
是因为有可能右边界右移的步数小于左边界右移的步数,并且仍然保持覆盖子串,因此即使 lChar
在 HashMap 中存在,我们仍然需要继续移动左指针,尝试缩小窗口。
count 的更新和 Hashmap 频率更新的先后顺序,在处理右边界字符时,是先更新count 然后更新 Hashmap 频率,但是在处理左边界字符时,是先更新 Hashmap 频率,然后更新count,请你分析这种先后顺序的必要性
你观察得非常精准,的确在处理右边界和左边界时,count
和 HashMap
频率的更新顺序不同。这种先后顺序是有必要的,因为它保证了算法在不同情况下的正确性和有效性。下面,我会详细分析为什么在右边界和左边界的处理过程中,count
和 HashMap
的更新顺序要有所区别。
右边界处理顺序:先更新 count
,再更新 HashMap
频率
if (map.containsKey(rChar)) {
if (map.get(rChar) > 0) {
count--; // 如果当前字符在t中且频率大于0,说明它是需要匹配的,count减1
}
map.put(rChar, map.get(rChar) - 1); // 更新字符频率,表示窗口已经包含了该字符
}
原因分析:
-
count
的含义:
count
表示窗口中还需要匹配的字符数量。也就是说,当count > 0
时,窗口还未完全覆盖字符串t
中的所有字符。当count == 0
时,窗口恰好包含了t
中的所有字符。 -
为什么先更新
count
?
在右边界扩展时,首先判断当前窗口能否满足字符匹配的需求。如果当前字符rChar
是目标字符串t
中的字符,并且它在HashMap
中的频率大于 0,说明这是一个仍然需要匹配的字符,因此我们首先更新count
,将它减 1,表示这个字符的匹配需求被满足了。这样做的原因是:
- 防止遗漏有效字符:在
count--
之前,我们需要确保这个字符是否真正是需要的。因为map.get(rChar) > 0
代表这个字符在窗口中还不够,因此我们首先减去count
表示该字符的匹配需求已被满足。 - 保证窗口的完整性:如果先更新了
HashMap
,可能会导致误判。假设先更新了HashMap
的频率值(即减去 1),然后再去检查count
,此时可能会因为HashMap
已经发生变化,而错过本应该减少的count
。
- 防止遗漏有效字符:在
-
更新
HashMap
频率:
在减少count
之后,再更新HashMap
,将rChar
的频率减 1,表示该字符已经被包含在窗口中。这一步保证了后续右指针继续移动时,窗口的状态是准确的。
左边界处理顺序:先更新 HashMap
频率,再更新 count
if (map.containsKey(lChar)) {
map.put(lChar, map.get(lChar) + 1); // 更新频率,表示移出窗口的字符需要重新匹配
if (map.get(lChar) > 0) {
count++; // 如果频率变为正数,说明需要再次匹配该字符,count增加
}
}
原因分析:
-
count
的含义保持不变:
count
在左边界移动时,仍然表示窗口中还需要匹配的字符数量。当窗口左边界字符被移出后,如果该字符属于t
中的字符,我们需要判断移出该字符后,窗口中是否还包含足够数量的该字符。 -
为什么先更新
HashMap
?
在左边界移动时,首先需要更新HashMap
的频率,表示我们即将移除左边界的字符lChar
。如果lChar
是t
中的字符,我们将它的频率加 1,表示窗口中少了一个这样的字符。这样做的原因是:
- 准确判断窗口状态:只有先更新了
HashMap
,我们才能准确判断窗口中是否缺少了某个字符。如果不先更新HashMap
,就无法知道这个字符移出后窗口是否仍然有效(即是否还包含足够的t
中字符)。 - 避免过早增加
count
:如果我们在更新HashMap
之前就更新count
,可能会错误地增加count
,而事实上,窗口中的字符数量可能仍然是足够的。
- 准确判断窗口状态:只有先更新了
-
更新
count
:
在HashMap
频率更新之后,我们检查频率是否变成了正数。如果是正数,说明窗口中的该字符数量已经不足,因此需要将count
增加,表示该字符需要重新匹配。
顺序对比总结:
-
处理右边界时:
- 先更新
count
是为了确保在当前字符进入窗口时,能够及时检查它是否满足匹配需求。只有在count
减少之后,才能表明该字符被正确匹配。 - 后更新
HashMap
是为了保证窗口的状态准确地反映已经包含的字符频率。
- 先更新
-
处理左边界时:
- 先更新
HashMap
是为了表示当前字符即将从窗口中移除。只有更新了字符的频率之后,才能判断窗口是否还满足条件。 - 后更新
count
是因为我们需要在频率变化之后才能判断该字符是否已经不足。如果移出字符后,窗口中还需要再次匹配该字符,那么count
才会增加。
- 先更新
必要性总结:
- 右边界的顺序确保了窗口扩展时能够正确减少
count
,保证每次右边界移动后,我们都能够实时判断窗口是否满足匹配条件。 - 左边界的顺序确保了在缩小窗口时,我们能够准确更新窗口的状态,防止误判窗口是否仍然满足所有条件。
这个先后顺序的设计,确保了算法在扩展和收缩窗口时,都能够正确地处理字符匹配问题,从而确保我们能找到最小的覆盖子串。