滑动窗口法
这个算法的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案么。该算法的大致逻辑如下:
int left = 0, right = 0;
while (right < s.size()) {
// 增大窗口
window.add(s[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
}
}
这个算法技巧的时间复杂度是 O(N),比一般的字符串暴力算法要高效得多。
滑动窗口的框架:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)
", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
这个框架是用c++写的,但套路和结构基本一样
用hashmap或者数组来记录目标字符串和窗口内的字符串的情况,一般都先把目标字符串情况统计出来
会设一个valid的值来统计窗口内“有用的”字符数
在循环内维护这个窗口,在一定条件下右移窗口或者左移窗口时都要更新窗口内的字符串的统计情况
实例1:
最小覆盖子串(leetcode76)
现在开始套模板,只需要思考以下四个问题:
1、当移动right
扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口?
3、当移动left
缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加window
计数器;如果一个字符将移出窗口的时候,应该减少window
计数器;当valid
满足need
时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
需要注意的是,当我们发现某个字符在window
的数量满足了need
的需要,就要更新valid
,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。
class Solution {
public String minWindow(String s, String t) {
if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) {
return "";
}
//维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
//ASCII表总长128
int[] need = new int[128];
int[] have = new int[128];
//将目标字符串指定字符的出现次数记录
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
//分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
//已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
int left = 0, right = 0, min = s.length() + 1, count = 0, start = 0;
while (right < s.length()) {
char r = s.charAt(right);
//说明该字符不被目标字符串需要,此时有两种情况
// 1.循环刚开始,那么直接移动右指针即可,不需要做多余判断
// 2.循环已经开始一段时间,此处又有两种情况
// 2.1 上一次条件不满足,已有字符串指定字符出现次数不满足目标字符串指定字符出现次数,那么此时
// 如果该字符还不被目标字符串需要,就不需要进行多余判断,右指针移动即可
// 2.2 左指针已经移动完毕,那么此时就相当于循环刚开始,同理直接移动右指针
if (need[r] == 0) {
right++;
continue;
}
//当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1
//是为了后续能直接判断已有字符串是否已经包含了目标字符串的所有字符,不需要挨个比对字符出现的次数
if (have[r] < need[r]) {
count++;
}
//已有字符串中目标字符出现的次数+1
have[r]++;
//移动右指针
right++;
//当且仅当已有字符串已经包含了所有目标字符串的字符,且出现频次一定大于或等于指定频次
while (count == t.length()) {
//挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
if (right - left < min) {
min = right - left;
start = left;
}
char l = s.charAt(left);
//如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
if (need[l] == 0) {
left++;
continue;
}
//如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
//就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
if (have[l] == need[l]) {
count--;
}
//已有字符串中目标字符出现的次数-1
have[l]--;
//移动左指针
left++;
}
}
//如果最小长度还为初始值,说明没有符合条件的子串
if (min == s.length() + 1) {
return "";
}
//返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
return s.substring(start, start + min);
}
}
leetcode567.字符串的排列
框架不变,但是左指针移动的条件变为长度>=length
class Solution {
public boolean checkInclusion(String s1, String s2) {
if(s1==null||s1==""||s2==null||s2==""||s2.length()<s1.length()){
return false;
}
int[] need=new int[128];
int[] have=new int[128];
int count=0;
int left=0;
int right=0;
for(int i=0;i<s1.length();i++){
need[s1.charAt(i)]++;
}
while(right<s2.length()){
char r=s2.charAt(right);
if(need[r]==0){
right++;
continue;
}
if(have[r]<need[r]){
count++;
}
have[r]++;
right++;
while(right-left>=s1.length()){
if(count==right-left){
return true;
}
char l=s2.charAt(left);
if(need[l]==0){
left++;
continue;
}
if(have[l]<=need[l]){
count--;
}
have[l]--;
left++;
}
}
return false;
}
}
这里的have其实只统计need中的那些字符,之外的“无效”字符是没有统计的,但无效字符仍然占有位置,这是需要微调的地方。