国外主流媒体网站三个律师做网站合适吗
LeetCode 76:最小覆盖子串

问题定义与核心挑战
给定字符串 s 和 t,需找到 s 中包含 t 所有字符(含重复)的最短子串。若不存在则返回空字符串。核心难点:
- 字符匹配的精确性:t中重复字符需在子串中对应数量匹配(如t="AA",子串需至少含 2 个A)。
- 高效区间搜索:直接枚举所有子串(O(n²))会超时,需通过 滑动窗口(双指针) 优化。
核心思路:滑动窗口 + 哈希表
利用 双指针(左 left、右 right) 维护动态窗口,结合 哈希表 跟踪字符频率:
- 扩展右指针:扩大窗口,记录字符频率,直到窗口包含 t所有字符。
- 收缩左指针:在窗口合法时,尝试左移缩小窗口,更新最小子串。
- 哈希表优化:通过 formed变量快速判断窗口是否合法(无需每次遍历哈希表)。
算法步骤详解
步骤 1:预处理 t 的字符频率
- 用哈希表 countT记录t中每个字符的出现次数。
- 计算 required:t中不同字符的数量(窗口需匹配这些字符的频率)。
Map<Character, Integer> countT = new HashMap<>();
for (char c : t.toCharArray()) {countT.put(c, countT.getOrDefault(c, 0) + 1);
}
int required = countT.size();
步骤 2:初始化滑动窗口变量
- left=0:窗口左边界。
- right=0:窗口右边界。
- formed=0:当前窗口中满足- t频率要求的字符数(如- t="ABC",窗口含- A、- B、- C各至少 1 个时,- formed=3)。
- windowCounts:记录当前窗口内字符的频率。
- minLen=∞,- start=0:记录最小窗口的长度和起始位置。
Map<Character, Integer> windowCounts = new HashMap<>();
int left = 0, formed = 0;
int minLen = Integer.MAX_VALUE;
int start = 0;
步骤 3:扩展右指针,构建窗口
遍历 s 的每个字符(右指针 right 移动):
- 更新窗口频率:将 s[right]加入windowCounts。
- 判断是否满足频率要求:若 s[right]在countT中,且windowCounts中其频率等于countT中的频率,则formed++。
- 当窗口合法(formed == required),尝试收缩左指针。
for (int right = 0; right < s.length(); right++) {char c = s.charAt(right);// 更新窗口频率windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);// 若当前字符是t的目标字符,且频率刚满足要求,formed加1if (countT.containsKey(c) && windowCounts.get(c).intValue() == countT.get(c).intValue()) {formed++;}// 窗口合法时,收缩左指针while (formed == required) {// 更新最小窗口int currentLen = right - left + 1;if (currentLen < minLen) {minLen = currentLen;start = left;}// 收缩左指针:移除s[left]char leftChar = s.charAt(left);windowCounts.put(leftChar, windowCounts.get(leftChar) - 1);// 若移除后,该字符频率不再满足t的要求,formed减1if (countT.containsKey(leftChar) && windowCounts.get(leftChar).intValue() < countT.get(leftChar).intValue()) {formed--;}left++; // 左指针右移}
}
步骤 4:返回结果
若找到合法窗口(minLen 未被更新为 ∞),则截取 s[start, start+minLen);否则返回空字符串。
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
关键逻辑解析
- 
formed变量的作用:
 避免每次检查整个windowCounts是否匹配countT(O(m)时间,m是t的不同字符数),而是通过formed实时跟踪已满足频率要求的字符数,达到O(1)判断窗口合法性。
- 
收缩左指针的条件: 
 仅当窗口合法(formed == required)时,才尝试收缩,确保每次收缩都在合法区间内进行,避免遗漏更短的合法窗口。
- 
字符频率的精确匹配: 
 仅当windowCounts[c]恰好等于countT[c]时,formed才增加;收缩时,若windowCounts[c]小于countT[c],formed才减少。这保证了formed仅统计完全满足频率要求的字符。
完整代码(Java)
import java.util.HashMap;
import java.util.Map;class Solution {public String minWindow(String s, String t) {// 步骤1:预处理t的字符频率和requiredMap<Character, Integer> countT = new HashMap<>();for (char c : t.toCharArray()) {countT.put(c, countT.getOrDefault(c, 0) + 1);}int required = countT.size();// 滑动窗口变量初始化Map<Character, Integer> windowCounts = new HashMap<>();int left = 0, formed = 0;int minLen = Integer.MAX_VALUE;int start = 0;// 步骤2:扩展右指针,构建窗口for (int right = 0; right < s.length(); right++) {char c = s.charAt(right);// 更新窗口内字符频率windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);// 若当前字符是t的目标字符,且频率刚满足要求,formed加1if (countT.containsKey(c) && windowCounts.get(c).intValue() == countT.get(c).intValue()) {formed++;}// 窗口合法时,收缩左指针while (formed == required) {// 更新最小窗口int currentLen = right - left + 1;if (currentLen < minLen) {minLen = currentLen;start = left;}// 收缩左指针:移除s[left]char leftChar = s.charAt(left);windowCounts.put(leftChar, windowCounts.get(leftChar) - 1);// 若移除后,该字符频率不再满足t的要求,formed减1if (countT.containsKey(leftChar) && windowCounts.get(leftChar).intValue() < countT.get(leftChar).intValue()) {formed--;}left++; // 左指针右移}}// 步骤3:返回结果return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);}
}
示例验证(以示例 1 为例)
输入:s = "ADOBECODEBANC", t = "ABC"
推导过程:
- 预处理:countT = {'A':1, 'B':1, 'C':1},required=3。
- 右指针扩展:
- 当 right=5(字符C),窗口[0,5](ADOBEC):windowCounts含A:1, B:1, C:1,formed=3,进入收缩阶段。
- 收缩左指针到 left=3(字符B),窗口[3,5](BEC):长度 3,记录为候选。
- 继续扩展右指针,最终找到窗口 [9,11](BANC),长度 4,为最小。
 
- 当 
复杂度分析
- 时间复杂度:O(n),其中n是s的长度。双指针各移动n次,哈希表操作均为O(1)。
- 空间复杂度:O(m),m是t中不同字符的数量(最多 26 个字母,故为O(1))。
该方法通过 滑动窗口 + 哈希表 高效解决了最小覆盖子串问题,核心在于动态维护窗口的合法性,并通过 formed 变量优化判断逻辑,确保了线性时间复杂度。
