76 最小覆盖子串
给你一个字符串 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 的子串中,
因此没有符合条件的子字符串,返回空字符串。
java
class Solution {
public String minWindow(String s, String t) {
int[] cnt = new int[1000];
for (int i = 0; i < t.length(); i++) {
cnt[t.charAt(i) - 'A']++;
}
HashSet<Character> set = new HashSet<>();
for (char a : t.toCharArray()) {
set.add(a);
}
int reFlag = 0;
int min = Integer.MAX_VALUE;
int l = 0;
String minStr = s;
int needCount = set.size();
int hasCount = 0;
for (int r = 0; r < s.length(); r++) {
int flag = 0;
char current = s.charAt(r);
if (set.contains(current)) {
// if(cnt[current-'A']>0){
cnt[current - 'A']--;
// }
// 翻了个大错误!:cnt[current-'A']==0 如果这里>0才--,会出错,直接hasCount!如果是S=AAAB t=AB 那么cnt[0]一开始是1。--。为0 直接hascount了!
if (cnt[current - 'A'] == 0) {
hasCount++;
}
}
while (hasCount == needCount && l <= r) {
// 移动左指针
String currentStr = s.substring(l, r + 1);
// System.out.println(currentStr);
if (currentStr.length() < min) {
reFlag = 1;
min = r - l + 1;
minStr = currentStr;
}
cnt[s.charAt(l) - 'A']++;
if (set.contains(s.charAt(l)) && cnt[s.charAt(l) - 'A'] > 0) {
hasCount--;
}
l++;
}
}
// if(t.length()>s.length()) return "";
if (reFlag == 0)
return "";
return minStr;
}
}
题解:
这道题很明显的滑动窗口题目:
使用set来存储t出现过的字符,方便后续的数组的计算。
使用数组来存储t出现的字符的次数。
使用needCount存储set.size()(如果达到了needCount表示当前子数组合法)
使用hasCount存储已经满足了几个字符
使用l表示慢指针
使用min和minstr来表示最小值和最短字符串
使用reflag表示最后到底是返回的全部字符串还是返回空字符串" 表示不满足题目"
首先使用快指针r循环遍历s字符串,如果碰到了字符串在set内,开始减数组出现的次数。如果是0表示这个字符已经满足出现的次数了,hascount++;
如果hasCount=needCount,表示字符串的所有字符都满足了出现字数的要求,开始移动左指针和判断最短字符串了! 得到min和minStr,开始移动l,并且如果移除的s.charAt[l]是在set里面,因为我们已经移除了,所以cnt必须要加了!如果加成0了,那么hasCount--了 !表示当前字符串不存在我们 这个字符了。
时间复杂度分析:
分析修改后的代码时间复杂度:
时间复杂度分析
您的算法是O(n),其中n是字符串s的长度。
详细分析:
外层循环:for(int r = 0; r < s.length(); r++)
执行n次(n = s.length())
内层while循环:
虽然看起来是嵌套循环,但每个字符最多被左指针和右指针各访问一次
左指针l只会向右移动,不会回退
因此,虽然有两个循环嵌套,但总操作次数是O(2n) = O(n)
其他操作:
初始化cnt数组:O(128) = O(1)
初始化set:O(m),其中m是t的长度
计算needCount:O(128) = O(1)
空间复杂度分析
cnt数组:固定大小128 = O(1)
set集合:O(m),其中m是t中不同字符的数量
其他变量:O(1)
总空间复杂度:O(m)
为什么是O(n)而不是O(n²)?
关键点:滑动窗口的左右指针都只向前移动,不会回退
右指针r:从0移动到n-1,共n步
左指针l:从0开始,最多移动到n-1,也是n步
虽然代码有嵌套循环,但每个字符最多被左右指针各访问一次
这类似于"两指针遍历数组"的模式,虽然代码结构是嵌套循环,但实际时间复杂度是线性的。
对比暴力解法:
如果使用暴力解法(检查所有子串),时间复杂度会是O(n³)或O(n²),而您的滑动窗口解法通过巧妙的指针移动将复杂度降到了O(n),这是相当高效的。
总结:算法的时间复杂度是O(n),空间复杂度是O(m),其中n是s的长度,m是t的长度
1234. 替换子串得到平衡字符串
提示
有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。
假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。
给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。
你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。
请返回待替换子串的最小可能长度。
如果原字符串自身就是一个平衡字符串,则返回 0。
示例 1:
输入:s = "QWER"
输出:0
解释:s 已经是平衡的了。
示例 2:
输入:s = "QQWE"
输出:1
解释:我们需要把一个 'Q' 替换成 'R',这样得到的 "RQWE" (或 "QRWE") 是平衡的。
示例 3:
输入:s = "QQQW"
输出:2
解释:我们可以把前面的 "QQ" 替换成 "ER"。
示例 4:
输入:s = "QQQQ"
输出:3
解释:我们可以替换后 3 个 'Q',使 s = "QWER"。
提示:
1 <= s.length <= 10^5
s.length 是 4 的倍数
s 中只含有 'Q', 'W', 'E', 'R' 四种字符
题目分析
题目的要求是找到一个「待替换子串」,通过替换这个子串,使得原字符串s变成一个「平衡字符串」。
也就是说原字符串可以看作两个部分:「待替换子串」 + 「不替换的内容」
那么我们就要求 「不替换的内容」中四个字符的出现次数必须小于等于n/4,这部分出现次数少于n/4的字符我们可以用「替换子串」去补充。
因此我们的目标就是,找到最短的「待替换子串」,使得「不替换的内容」中四个字符的出现次数小于等于n/4。
那么我们如何去搜索最短的「待替换子串」,或者说我们对于当前得到的一个子串如何去进行进一步的搜索。我们记一个可以使得「不替换的内容」中四个字符的出现次数小于等于n/4的「待替换子串」为合法子串。
如果当前得到的子串是一个合法子串,那么我们考虑去缩短这个子串,看看有没有可能找到更短的合法子串;
如果当前得到的子串不是合法子串,那么我们必须去延伸这个子串。【子串不合法,就说明「不替换的内容」中存在至少一个字符的出现次数大于n/4,我们就要通过加长子串,从而缩短「不替换的内容」,期望多的字符被加入子串,使得「不替换的内容」可能满足】
这样的搜索方式我们考虑使用一个滑动窗口[left, right]来维护搜索到的子串, 我们从left=right=0即第一个字母构成的子串开始搜索。
如果当前子串是一个合法子串,left右移缩短字符串;如果left已经到达字符串尾部,说明没有缩小的空间,那么就结束搜索。
如果当前子串不是一个合法子串,right右移延伸字符串;如果right已经到达字符串尾部,说明没有延伸的空间,那么就结束搜索。
因为我们在搜索过程中要时刻判断「不替换的内容」中四个字符的出现次数是否小于等于n/4,因此我们必须先遍历一遍整个字符串,得到字符的总共出现次数【初始化断「不替换的内容」中四个字符的出现次数,表示初始子串为空】,然后在滑动窗口的移动过程中,动态的增加或减少对应字符出现的次数,来维护「不替换的内容」中四个字符的出现次数。
下图显示了滑动窗口的一个搜索过程:

java
class Solution {
public int balancedString(String s) {
HashMap<Character, Integer> map = new HashMap<>();
int l = 0;
int min = Integer.MAX_VALUE;
if (s.length() % 4 != 0)
return 0;
int cnt = s.length() / 4;
for (int r = 0; r < s.length(); r++) {
char a = s.charAt(r);
map.put(a, map.getOrDefault(a, 0) + 1);
}
if(check(map,cnt)) return 0;
for (int r = 0; r < s.length(); r++) {
// 不合法,移动右指针
map.put(s.charAt(r),map.getOrDefault(s.charAt(r),0)-1);
while (l<=r&&check(map, cnt)) {
min=Math.min(min,r-l+1);
map.put(s.charAt(l),map.getOrDefault(s.charAt(l),0)+1);
l++;
}
}
return min;
}
public boolean check(HashMap<Character, Integer> map, int cnt) {
if (map.getOrDefault('W', 0) > cnt || map.getOrDefault('E', 0) > cnt || map.getOrDefault('Q', 0) > cnt
|| map.getOrDefault('R', 0) > cnt)
return false;
else
return true;
}
}
/**
我们需要找到一个最短的窗口 [l, r],把它替换后,使得剩余的部分都 ≤ n/4。
步骤:
统计每个字符的出现次数。
如果所有字符出现次数 ≤ n/4,直接返回 0。
否则使用 滑动窗口:
移动右指针 r,不断减少 map[s[r]]。
当当前剩余字符的数量都 ≤ n/4,说明 [l, r] 这段替换即可。
更新最小窗口长度,并尝试右移左指针 l。
*/