滑动窗口算法:
int left =0,right =0;
while(right<s.size()){
//增大窗口
window.add(s[right]);
right++;
while(window needs shrink){
//缩小窗口
window.remove(s[left]);
left++;
}
}
其实对应的算法思路很简单,主要是细节比较繁琐。 比如如何向窗口添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。
滑动窗口套路框架:
void slidingWindow(String s,String t){
Map<char,Integer> need = new HashMap<>();
Map<char,Integer> window = new HashMap<>();
for(char c:t) need.put(c,map.getOrDefault(c,0)+1);
int left =0,right =0;
int valid =0;
while (right<s.size()){
//c是将移入窗口的字符
char c = s[right];
//右移窗口
right++;
//进行窗口内数据的一系列更新
...
//debug输出的位置
System.out.println("window:[%d,%d)\n",left,right);
//判断左侧窗口是否要收缩
while(window needs shrink){
//d是将移出窗口的字符
char d = s[left];
left++;
//进行窗口数据的一系列更新;
}
}
}
实际上,增大right是在找可行解,找到可行解后,通过增大left来找最优解(即优化)。
在框架中使用了needs和window字典,他们充当的是计数器的功能。needs用来记录T中每个字符出现的次数,即:我们所需要的字符的情况;window字典则是用来记录当前实际窗口中包含对应字符的情况,通过两个map的对比,来判断是否满足了条件。
回到这道题:
首先,我们初始化needs和window两个哈希表,记录需要凑齐的字符和当前窗口中的字符情况。
String minstr(String s,String t){
Map<Char,Integer> needs = new HashMap<>();
Map<Char,Integer> window = new HashMap<>();
for(Char c:t) needs.put(c,map.getOrDefault(c,0)+1);
然后,使用left和right变量初始化窗口两端,区间为左闭右开,这样保证了一开始窗口中没有任何元素。
int left=0,right=0;
int valid =0;
while (right<s.size()){
//开始滑动
}
其中,valid变量表示已经满足条件的字符的个数,如果valid和need中key的数量相等(needs.size()),则说明窗口已满足条件,完全覆盖了T。
接下来需要考虑的问题是:
完整代码,但是存在很多细节的小问题,后面会一一解答:
String minstr(String s,String t){
Map<Char,Integer> needs = new HashMap<>();
Map<Char,Integer> window = new HashMap<>();
for(Char c:t) needs.put(c,needs.getOrDefault(c,0)+1); //java中不可以这样取单个字符,需要通过函数charAt
int left=0,right=0;
int valid =0;
//记录最小覆盖子串的起始索引及长度
int start =0,len=Integer.MAX_VALUE;
while (right<s.length()){ //java中字符串的长度用length()函数
//c是将移入窗口的字符
Char c = s[right]; //这里存在问题,java中需要通过charAt(索引)
right++;
//进行窗口一系列数据的更新
if(needs.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
/*这里不能使用“==”,因为Integer类型值超出[-128,127]范围会new一个新的对象,
等于是比较的对象值,所以应该用equals。*/
if(window[c] == needs[c])
valid++;
}
while(valid == need.size()){
//更新最小覆盖子串
if(right-left <len){
start = left;
len = right - left;
}
//d是将要移出窗口的字符
Char d = s[left]; //同上
left++;
if(needs.containsKey(d)){
if(window[d] == needs[d]) //同上
valid--;
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
//返回最小子串
return len == Integer.MAX_VALUE?"":s.substr(start,len); // java中截取字符串中的子串,使用substring函数,且参数应为(起始index,终止index)
修改后的代码如下:
public String minWindow(String s,String t){
Map<Character,Integer> needs = new HashMap<Character,Integer>();
Map<Character,Integer> window = new HashMap<Character,Integer>();
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
needs.put(c,needs.getOrDefault(c,0)+1);
}
int left=0,right=0;
int valid =0;
//记录最小覆盖子串的起始索引及长度
int start =0,len=Integer.MAX_VALUE; boolean sign = false;
while (right<s.length()){
//c是将移入窗口的字符
char c = s.charAt(right);
right++;
//进行窗口一系列数据的更新
if(needs.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(needs.get(c)))
valid++;
}
while(valid == needs.size()){
sign = true;
//更新最小覆盖子串
if(right-left <len){
start = left;
len = right - left;
}
//d是将要移出窗口的字符
char d = s.charAt(left);
left++;
if(needs.containsKey(d)){
if(window.get(d).equals(needs.get(d)))
valid--;
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
//返回最小子串
return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
}
关于Integer和int的比较以及equals和“==”的使用,下面这个链接有很详细的讲解: https://blog.csdn.net/andyzhaojianhui/article/details/84324466
这道题跟上面那道题不同的地方在于:这道题中s2是要包含s1这个子串或者这个子串的排列的,也就是说在s2中搜索的子串大小就应该为2
public boolean checkInclusion(String s1, String s2) {
Map<Character,Integer> needs = new HashMap<>();
Map<Character,Integer> window = new HashMap<>();
int sz = s1.length();
for(int i=0;i<sz;i++){
char c = s1.charAt(i);
needs.put(c,needs.getOrDefault(c,0)+1);
}
int left =0,right =0;
int valid =0;
while(right < s2.length()){
char c = s2.charAt(right);
right++;
if(needs.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(needs.get(c)))
valid++;
}
while(right-left>=s1.length()){
if(valid == needs.size())
return true;
char d = s2.charAt(left);
left++;
if(needs.containsKey(d)){
if(window.get(d).equals(needs.get(d)))
valid--;
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
return false;
}
public List<Integer> findAnagrams(String s, String p) {
Map<Character,Integer> needs = new HashMap<>();
Map<Character,Integer> window = new HashMap<>();
ArrayList<Integer> res = new ArrayList<>();
if(s.length()<p.length()) return res;
int sz = p.length();
for(int i =0;i<sz;i++){
char c = p.charAt(i);
needs.put(c,needs.getOrDefault(c,0)+1);
}
int left =0, right=0;
int valid=0;
while(right<s.length()){
char c = s.charAt(right);
right++;
if(needs.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(needs.get(c).equals(window.get(c)))
valid++;
}
while(right-left>=p.length()){
if(valid == needs.size()){
res.add(left);
}
char d = s.charAt(left);
left++;
if(needs.containsKey(d)){
if(needs.get(d).equals(window.get(d)))
valid--;
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
return res;
}
public int lengthOfLongestSubstring(String s) {
Map<Character,Integer> window = new HashMap<>();
if (s.length()==0) return 0;
int left =0,right=0;
int MAX_str = Integer.MIN_VALUE;
// ArrayList res = new ArrayList<>();
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.getOrDefault(d,0)-1);
}
MAX_str = Math.max(MAX_str,right-left);
}
return MAX_str;
}
这实际上把问题变简单了。但是需要注意的是,如何判断收缩窗口的时机,这里是使用window[c]>1,这说明窗口中存在重复字符。 还有就是在哪儿更新res,因为我们要的是最长无重复子串,所以只要找到一个阶段保证窗口中没有重复字符串就行。