目录
零. 模板
public static void slidingWindow(String str){
//用合适的数据结构记录窗口的数据
HashMap<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
while(right < str.length()){
//将c移入窗口
char c = str.charAt(right);
window.put(c, window.getOrDefault(c, 0) + 1);[添加链接描述](https://leetcode.cn/problems/minimum-window-substring/)
//增大窗口
right++;
//进行窗口数据的一些列更新......这里以输出left right为例子,可删除
System.out.println("left" + left + "_"+ "right:" + right);
}
//判断左窗口是否需要收缩
while(left < right && window needs shrink){
//d是移出窗口的字符
char d = str.charAt(left);
window.put(d, window.getOrDefault(left, 0) - 1);
//缩小窗口
left++;
//进行窗口数据的更新......
}
}
- 时间复杂度:O(n),n为字符串/数组的长度
一. 最小覆盖子串
思路和代码:
I. 博主的做法
不会,,太难了
II. 东哥的做法
-
第零步:构建两个map
- needs:用来记录给定子串 t 中的字符及存在的个数
- window:用来记录窗口 window 中的字符及存在的个数
-
第一步:遍历 字符串 t,将里面的字符及个数存入needs里
-
第二步:将窗口向右扩展,如果这个字符在 needs 中,更新window 和 valid,直到 window 包含了 t 中所有的字符
-
第三步:检查 valid 与 needs 大小是否相等,如果相等,说明我们的窗口已经满足条件,可以左边缘右移
-
第四步:如果子串长度小于上一个子串长度 -> 更新子串(子串信息由 start 和 len 来存储)
-
第五步:窗口左边缘右移,更新window,如果window[c_out] 内的出现次数和 need[c_out] 相等,更新 valid
- 只有当 window[c_out] 内的出现次数和 need[c_out] 相等时,才能 -1(左移0)
- 如果 window[c_out] < need[c_out],第二个while循环根本进不来,不可能
- 如果 window[c_out] > need[c_out],那么就算c_out移出去也无所谓,依然满足window包含 t 中所有字符
- 即使左移不满足要求了,也没关系,left刚好指的是新字符串的起始位置,寻找新的子串
- 只有当 window[c_out] 内的出现次数和 need[c_out] 相等时,才能 -1(左移0)
-
第六步: 一直重复第二步 ~ 第四步,直到 right 最终来到了字符串 s 的尾部
class Solution {
public String minWindow(String s, String t) {
//第零步
Map<Character, Integer> needs = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int valid = 0;
int start = 0, len = Integer.MAX_VALUE;
//第一步
for(char c : t.toCharArray())
needs.put(c, needs.getOrDefault(c,0) + 1);
//第六步(重复)
while(right < s.length()){
//第二步
char c_in = s.charAt(right);
right++;
//第三步
if(needs.containsKey(c_in)){
window.put(c_in, window.getOrDefault(c_in, 0)+1);
if(window.get(c_in).equals(needs.get(c_in)))
valid++;
}
while(valid == needs.size()){
//第四步
if(right - left < len){
start = left;
len = right - left;
}
//第五步
char c_out = s.charAt(left);
left++;
if(needs.containsKey(c_out)){
//如果刚刚好,那么才能减一。如果window多于needs,那么移出去也不影响
//当然,如果window < needs,那这个while循环都进不来
if(window.get(c_out).equals(needs.get(c_out)))
valid--;
//如果valid减小了,也无所谓,left刚好指的是新字符串的起始位置,寻找新的子串
window.put(c_out, window.get(c_out)-1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len);
}
}
- 时间复杂度:O(|S| + |T|),|S| 表示字符串 s 的长度,|T| 表示字符串 t 的长度
- 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度
- 如果有重复字母,needs.size() 就比 t.length() 小
二. 字符串的排列
思路和代码:
I. 博主的做法
博主本来想用给的 s1 子串,算出所有的全排列再放到滑动窗口里。仔细想想太麻烦了。
II. 东哥的做法
- 和上个题非常的像,只是在左边缘右移的时候有些变化
- 所谓全排列,其实就是:串的长度一样并且里面各个字符的个数也相同。
- 第零步:构建needs,window两个hashmap
- 第一步:将 s1 的值放入 needs 中
- 第二步:窗口右移
- 第三步:如果窗口的长度等于 s1 的长度了
- 如果 valid 和 needs 的大小相同,也就是找到了子串,返回true
- 如果不同,那么窗口左边缘右移
- 第四步:重复第二 ~ 四步,直到 right 来到了 s2 串的尾部
- 这里需要注意的是:里面的 while 可以换成 if ,因为这个题是固定窗口大小的(s1的长度),最多滑动也只是 1 个元素
- 这两句的顺序一定不能换:一点是先判断是否满足需求,再进行 -1 操作!!!!!!!
if(needs.get(c_out).equals(window.get(c_out)))
valid--;
window.put(c_out, window.get(c_out) - 1);
class Solution {
public boolean checkInclusion(String s1, String s2) {
//第零步
Map<Character, Integer> needs = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
//第一步
for(char c : s1.toCharArray())
needs.put(c, needs.getOrDefault(c,0)+1);
int left = 0, right = 0;
int valid = 0;
//第四步(重复)
while(right < s2.length()){
//第二步
char c_in = s2.charAt(right);
right++;
if(needs.containsKey(c_in)){
window.put(c_in, window.getOrDefault(c_in, 0) + 1);
if(window.get(c_in).equals(needs.get(c_in)))
valid++;
}
//写成这样也可以:if(right - left == s1.length()){
//窗口扩大到 s1 的长度就可以了
while(right - left == s1.length()){
//第三步
if(valid == needs.size())
return true;
char c_out = s2.charAt(left);
left++;
if(needs.containsKey(c_out)){
if(needs.get(c_out).equals(window.get(c_out)))
valid--;
window.put(c_out, window.get(c_out) - 1);
}
}
}
return false;
}
}
- 时间复杂度:O(|S| + |T|),|S| 表示字符串 s2 的长度,|T| 表示字符串 s1 的长度
- 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度
三. 找到字符串中所有字母异位词
思路和代码:
- 和上道题基本上一模一样,只不过这次要返回每一个子串的首地址
- 我们申请一个ArrayList,每次找到,将首地址加进去就好了
- 需要注意的是:needs.size() 不一定等于 p.length()
- 如果有重复字母,那么needs.size() 只算一次
- eg:String p = “11111”;
- needs.size() = 1; p.length() = 5;
- 在子串判断的时候一定要使用 needs.size() == valid,而不是使用 p.length()
- 如果有重复字母,那么needs.size() 只算一次
I. 博主的做法
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> needs = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
List<Integer> result = new ArrayList<>();
int left = 0, right = 0;
int valid = 0;
for(char c : p.toCharArray())
needs.put(c, needs.getOrDefault(c, 0) + 1);
while(right < s.length()){
char c_in = s.charAt(right);
right++;
if(needs.containsKey(c_in)){
window.put(c_in, window.getOrDefault(c_in, 0) + 1);
if(window.get(c_in).equals(needs.get(c_in)))
valid++;
}
while(right - left == p.length()){
if(valid == needs.size())
result.add(left);
char c_out = s.charAt(left);
left++;
if(needs.containsKey(c_out)){
if(needs.get(c_out).equals(window.get(c_out)))
valid--;
window.put(c_out, window.get(c_out) - 1);
}
}
}
return result;
}
}
II. 东哥的做法
- 和博主做的一样
- 时间复杂度:O(|S| + |T|),|S| 表示字符串 s 的长度,|T| 表示字符串 p 的长度
- 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度
四. 最长无重复子串
思路和代码:
I. 博主的做法
- 博主这里,将map换成了ArrayList,其实是一样的,换成队列也可以
- 第一步:如果 window 里没有c_in,那我们就加入,窗口右移
- 第二步:if 是针对不同字母的
- 如果都是不同字母,那窗口一直右移动就可以,不断更新 max
- 多写了一遍:max = max < len ? len : max; 是防止 s 都是不同的字母,根本不经过 else
- 第三步:else 是针对有重复字母的
- 如果有重复字母(right 指向的是重复字母),那我们让 right 回退一格,始终不让它前进,left 右移,缩小窗口(这里处理的是eg:abb;这种重复,最终 left 和 right 重合,window 里只剩下 b)
class Solution {
public int lengthOfLongestSubstring(String s) {
int left = 0, right = 0;
int len = 0;
int max = 0;
List<Character> window = new ArrayList<>();
while(right < s.length()){
char c_in = s.charAt(right);
right++;
if(!window.contains(c_in)){
window.add(c_in);
len++;
max = max < len ? len : max;
}
else{
max = max < len ? len : max;
char c_out = s.charAt(left);
window.remove((Object)c_out);
left++;
right--;
len--;
}
}
return max;
}
}
- 时间复杂度:O(|S| ),|S| 表示字符串 s 的长度
- 空间复杂度:O(|S|),这里使用了一个 hashmap 来存储数据,最长不会超过 s 的长度
II. 东哥的做法
- 第一步:不管是啥,先窗口右移
- 第二步:如果当前的 c 多于一个,不停的缩小窗口,直到 c 最后是一个
- 第三步:不停的更新 res
int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
window.put(c, window.getOrDefault(c, 0) + 1);
// 判断左侧窗口是否要收缩
while (window.get(c) > 1) {
char d = s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
window.put(d, window.get(d) - 1);
}
// 在这里更新答案
res = Math.max(res, right - left);
}
return res;
}
- 时间复杂度:O(|S| ),|S| 表示字符串 s 的长度
- 空间复杂度:O(|S|),这里使用了一个 hashmap 来存储数据,最长不会超过 s 的长度
参考:https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/wo-xie-le–f7a92/