leetcode全部滑动窗口题目总结C++写法(完结)

3. 无重复字符的最长子串

A:

要找最长的无重复子串,所以用一个map保存出现过的字符,并且维持一个窗口,用le和ri指针标识。ri为当前要遍历的字符,如果ri字符在map中出现过,那么将le字符从map移除,le++。如果ri字符没出现过,那ri++,并更新最大无重复子串长度。全程利用map保持无重复的要求,每次循环要么le++,要么ri++。最差情况是n个一样的字符,那么ri++,le++,ri++,le++这样一直从0循环到n-1。时间复杂度和空间复杂度都是O(N)。

 1 class Solution {
 2 public:
 3     int lengthOfLongestSubstring(string s) {
 4         if(s.empty()){return 0;}
 5         int le=0,ri=1;
 6         map<char,int> dic;
 7         dic[s[0]]+=1;
 8         int max_len=1;
 9         while(ri<s.size()){
10             if(dic[s[ri]]){
11                 dic[s[le]]-=1;
12                 ++le;
13             }
14             else{
15                 dic[s[ri]]+=1;
16                 ++ri;
17                 max_len=max(max_len,ri-le);
18                 
19             }
20         }
21         return max_len;
22     }
23 };

在这里插入图片描述

30. 串联所有单词的子串

A:

题目指定了所有单词是一样长度的,那么我们就可以按单词为单位进行查找。假如单词长度为3,那么从0、1、2分别按单词长度进行查找,一直查找到字符串结尾,就可以查找到所有满足条件的子串。

 1 class Solution {
 2 public:
 3     vector<int> findSubstring(string s, vector<string>& words) {
 4         if(s.empty() or words.empty()){
 5             return vector<int>();
 6         }
 7         int s_len=s.length(),one_word=words[0].size(),all_word=words.size()*one_word,word_cnt=words.size();
 8         if(all_word>s_len){
 9             return vector<int>();
10         }
11         map<string,int> dic;
12         for(const string& word:words){
13             dic[word]+=1;
14         }
15         vector<int> res;
16         for(int i=0;i<one_word;++i){
17             int le=i,ri=i,cnt=0;
18             map<string,int> cur_dic;
19             while(ri+one_word-1<s_len){    //ri开头的单词结尾不能越界
20                 string cur=s.substr(ri,one_word);    //当前单词
21                 if(dic[cur]==0){    //无匹配
22                     ri+=one_word;
23                     le=ri;
24                     cnt=0;
25                     cur_dic.clear();
26                 }
27                 else{    //匹配
28                     ri+=one_word;
29                     cur_dic[cur]+=1;
30                     ++cnt;
31                     while(dic[cur]<cur_dic[cur]){
32                         string w=s.substr(le,one_word);
33                         cur_dic[w]-=1;
34                         le+=one_word;
35                         --cnt;    
36                     }
37                     if(cnt==word_cnt){
38                         res.push_back(le);
39                     }
40                 }
41             }
42         }
43         return res;
44     }
45 };

在这里插入图片描述
但是稍微改动一下:

 1 class Solution {
 2 public:
 3     vector<int> findSubstring(string s, vector<string>& words) {
 4         if(s.empty() or words.empty()){
 5             return vector<int>();
 6         }
 7         int s_len=s.length(),one_word=words[0].size(),all_word=words.size()*one_word,word_cnt=words.size();
 8         if(all_word>s_len){
 9             return vector<int>();
10         }
11         map<string,int> dic;
12         for(const string& word:words){
13             dic[word]+=1;
14         }
15         vector<int> res;
16         for(int i=0;i<one_word;++i){
17             int le=i,ri=i,cnt=0;
18             unordered_map<string,int> cur_dic;
19             while(ri+one_word-1<s_len){    //ri开头的单词结尾不能越界
20                 string cur=s.substr(ri,one_word);    //当前单词
21                 if(dic.count(cur) and cur_dic[cur]<dic[cur]){
22                     ++cnt;
23                     cur_dic[cur]+=1;
24                     ri+=one_word;
25                 }
26                 else{
27                     if(dic.count(cur)==0){    //ri开头的单词不在单词表里
28                         ri+=one_word;    //ri后移一个单词长度
29                         if(ri+one_word-1>=s_len){
30                             break;
31                         }
32                         //因为之前ri开头的单词gg了,那么移动le到新的ri重新开始
33                         le=ri;
34                         cur_dic.clear();
35                         cnt=0;
36                     }
37                     else{    //cur_dic[cur]>=dic[cur],le一直右移直到cur_dic[cur]<dic[cur]使得cur能插入
38                         while(cur_dic[cur]>=dic[cur]){
39                             cur_dic[s.substr(le,one_word)]-=1;
40                             le+=one_word;
41                             --cnt;
42                         }
43                     }
44                 }
45                 if(cnt==word_cnt){
46                     res.push_back(le);
47                     cur_dic[s.substr(le,one_word)]-=1;
48                     le+=one_word;
49                     --cnt;
50                 }
51             }
52         }
53         return res;
54     }
55 };

在这里插入图片描述
改动的地方只是把dic[cur]==0改成了dic.count(cur)==0。对于dic中不存在的cur,前者会自动假如加入 dic,默认值为0。所以内存用量一下从14M变成了26M,同时map的查询时间是log级别,所有的查map时间也会有相应增加,这一点一定要注意。

76. 最小覆盖子串

A:

写了两遍,代码基本的结构一致,唯一一点区别是第一种是先一直向右寻找直到找到覆盖t的子串,再尽量把左边界右移加入结果。
第二种是每次循环只向右跳一个字符,如果还没找到覆盖t的子串就记录并把左边界尽量右移,找到的话就加入结果。其实写到这我发现这俩解法没啥区别,就不说了吧。
另外看见题解里有一种解法是把s串中属于t串的字符挑出来记录成map,这样在遍历s寻找覆盖t的子串的时候就不用实际在s中从0到s.size()-1慢慢挪了,相当于把不属于t串的字符直接一下子跳过。在s串长度远大于t的时候能节省大量时间,这个写法有时间再补吧。。2019年11月13日 22:19:10

 1 class Solution {
 2 public:
 3     string minWindow(string s, string t) {
 4         if(s.empty() or t.empty()){
 5             return string();
 6         }
 7         map<char,int> dic,cur_dic;
 8         for(const char& c:t){
 9             dic[c]+=1;
10         }
11         int le=0,ri=0,min_len=INT_MAX,cnt=0,t_len=t.size();
12         string res="";
13         while(ri<s.size()){
14             // cout<<le<<" "<<ri<<" "<<s.substr(le,ri-le)<<" "<<cnt<<endl;
15             while(ri<s.size() and cnt<t_len){
16                 if(dic.count(s[ri])>0){
17                     if(dic[s[ri]]>cur_dic[s[ri]]){
18                         cnt++;
19                     }
20                     cur_dic[s[ri]]++;
21                 }
22                 ++ri;
23             }
24             if(cnt!=t_len){
25                 return res;
26             }
27             while(dic.count(s[le])==0 or dic[s[le]]<cur_dic[s[le]]){
28                 if(dic.count(s[le])!=0){
29                     cur_dic[s[le]]--;
30                 }
31                 ++le;
32             }
33             if(ri-le<min_len){
34                 min_len=ri-le;
35                 res=s.substr(le,ri-le);
36             }
37             cur_dic[s[le]]--;
38             ++le;
39             cnt--;
40         }
41         return res;
42     }
43 };
 1 class Solution {
 2 public:
 3     string minWindow(string s, string t) {
 4         if(s.empty() or t.empty()){
 5             return string();
 6         }
 7         map<char,int> dic,cur_dic;
 8         for(const char& c:t){
 9             dic[c]+=1;
10         }
11         int le=0,ri=0,min_len=INT_MAX,cnt=0,t_len=t.size();
12         string res="";
13         while(ri<=s.size()){
14             // cout<<le<<" "<<ri<<" "<<s.substr(le,ri-le)<<" "<<cnt<<endl;
15             char c=s[ri];
16             ++ri;
17             if(dic.count(c)){   //匹配
18                 cur_dic[c]+=1;
19                 if(dic[c]>=cur_dic[c]){
20                     ++cnt;
21                 }
22                 while(1){   //有重复匹配的,一直右移左边界
23                     while(dic.count(s[le])==0){    //跳过开头多余字符
24                         ++le;
25                     }
26                     if(dic[s[le]]<cur_dic[s[le]]){
27                         cur_dic[s[le]]-=1;
28                         ++le;
29                     }
30                     else{
31                         break;
32                     }
33                 }
34                 while(dic.count(s[le])==0){
35                     ++le;
36                 }
37                 if(cnt==t_len and ri-le<min_len){
38                     cout<<s.substr(le,ri-le)<<endl;
39                     min_len=ri-le;
40                     res=s.substr(le,ri-le);
41                 }
42             }
43             else{   //不匹配
44                 ;
45             }
46         }
47         return res;
48     }
49 };

可以看到两种解法时间并不算快,因为是遍历了s的全部字符。
在这里插入图片描述

159. 至多包含两个不同字符的最长子串

 1 class Solution {
 2 public:
 3     int lengthOfLongestSubstringTwoDistinct(string s) {
 4         map<char,int>dic;
 5         int le=0,ri=0,cnt=0,s_len=s.size(),max_len=0;
 6         while(ri<s_len){
 7             if(cnt==2){
 8                 if(dic.count(s[ri])){   //是之前的两个字符之一,直接添加
 9                     dic[s[ri]]++;
10                     ++ri;
11                 }
12                 else{   //不属于之前的两个字符
13                     while(dic[s[le]]>1){    //找最左侧元素出现次数为1的
14                         dic[s[le]]-=1;
15                         ++le;
16                     }
17                     dic.erase(s[le++]);   //把这个出现次数为1的删了
18                     dic[s[ri]]++;
19                     ++ri;
20                 }
21             }
22             else{   //cnt=0 or 1
23                 if(dic.count(s[ri])==0){
24                     ++cnt;
25                 }
26                 dic[s[ri++]]+=1;
27             }
28             max_len=max(max_len,ri-le);
29         }
30         return max_len;
31     }
32 };

在这里插入图片描述

209. 长度最小的子数组

这题不解释了阿,前面会了就会。

A:

 1 class Solution {
 2 public:
 3     int minSubArrayLen(int s, vector<int>& nums) {
 4         int le=0,ri=0,len=nums.size(),min_len=INT_MAX,cur_sum=0;
 5         while(ri<len){
 6             cur_sum+=nums[ri++];
 7             if(cur_sum>=s){
 8                 while(cur_sum-nums[le]>=s){
 9                     cur_sum-=nums[le];
10                     ++le;
11                 }
12                 min_len=min(min_len,ri-le);
13             }
14         }
15         return min_len!=INT_MAX?min_len:0;
16     }
17 };

在这里插入图片描述

239. 滑动窗口最大值

这题没做出来,用的是一个新的东西:双端队列。
我先说一下我做时候的想法:由于这题的窗口是固定长度,每次要找窗口内的最大值。那最naive的方法就是维护长度k的窗口,遍历(n-k)*k次就行,复杂度O(nk)。然后我又考虑可以用堆(c++优先队列)维护窗口,这样复杂度可以减少为O(nlogk)。但问题是假如前一个窗口的最大值是首元素,窗口后移一位后,怎么把之前最大值删掉。更新:这里可以用索引堆,专门去研究了一下索引堆怎么写。 但这个写法就不写了。
下面只放一个双端队列的做法:
思路是维护一个双端队列(两边都可以push和pop的队列),其大小为k。然后关键要始终维持的性质是该队列为非递增序,即x1>=x2>=x3>=…>=xk。当然队列不存储值,而是存储对应nums数组中的下标。然后每次窗口右移一位,每次要考察队首(队列左侧)的值是否已经越界。即当前窗口右侧到达i位置,那么最左侧位置不能小于i+1-k,否则整个窗口长度就超过k了,这就不符题意了。而且每次滑动窗口只需要检验最左侧,原因有二:1. 我们push索引到窗口里的时候是从左到右遍历nums来的,故只要最左侧不越界,窗口其它位置一定不越界。2. 由于滑动窗口一次只挪一位。
上面一段写成代码就是:

if(!window.empty() and window.front()<i-k+1){
                window.pop_front();
            }

考察最左侧索引后,剩下的工作就是push新移进窗口的右边界(新的边界)。
同最开始建立初始窗口一样,我们要保持非增序。小于右边界的都pop,最后把右边界push进来,就完成了一轮窗口的移动。对应的代码如下:

while(!window.empty() and nums[window.back()]<nums[i]){
                window.pop_back();
            }
            window.push_back(i);

这样完成一轮移动后,窗口内最大的元素就是左边界作为索引p对应的nums中的元素nums[p]。
下面我简单证明一下这个算法的有效性。
1.对于任意长度k的子数组,我们对左边界是否越界的考察保证了当前窗口不会包含比该长度k子数组更多的元素。
2.既然我们的窗口包含元素比实际长度k的子数组更少,那有没有可能会略掉某个该子数组中的最大值没有统计呢?假设实际子数组是i到i+k-1,那么上次的窗口遍历的是i-1到i+k-2的子数组,并假设上次的窗口window1中的元素都满足不越界(相当于数学归纳法,假设i-1的情况成立,只要推得i的情况成立,并且i==1时情况成立结论就成立,而这题我们第一轮初始的窗口显然是正确的)。1. 假设上次的窗口是window1。若window1包含i-1,则当前这轮遍历开始时,会考察左边界是否越界,此时会把i-1剔除掉,再push进来i+k-1号。故这种情况当前窗口会输出正确结果。
2. 假设上次的窗口window1不包含i-1,那么显然这轮遍历开始时,左边界肯定不会越界,因为左边界最多也就是i,i本来就应该在当前窗口内的。故这种情况也会输出正确结果。

 1 class Solution {
 2 public:
 3     vector<int> maxSlidingWindow(vector<int>& nums, int k) {
 4         if(k<1 or nums.empty()){
 5             return vector<int>();
 6         }
 7         deque<int> window;
 8         int siz=nums.size();
 9         vector<int> res;
10         //初始化双端队列
11         for(int i=0;i<k;++i){
12             while(!window.empty() and nums[window.back()]<nums[i]){
13                 window.pop_back();
14             }
15             window.push_back(i);
16         }
17         res.push_back(nums[window.front()]);
18         for(int i=k;i<siz;++i){
19             if(!window.empty() and window.front()<i-k+1){
20                 window.pop_front();
21             }
22             while(!window.empty() and nums[window.back()]<nums[i]){
23                 window.pop_back();
24             }
25             window.push_back(i);
26             res.push_back(nums[window.front()]);
27         }
28         return res;
29     }
30 };

在这里插入图片描述

567. 字符串的排列

这题其实只要前面题都做了,就一模一样的解法套就完事了。然后我看评论中很多用vector代替map来做字符记录的。这种做法是利用char对应ascii码0到128的特点,开一个128大小的vector来模拟map,比如当前有一个字母a(ascii好像是97吧),那就把vector[97]+1,到时候查询字母a的频数也直接来查vector[97]就好了。滑窗还是正常的滑,并且这题还是固定大小的窗口,所以比较简单。用map会慢一些,因为首先map查询O(logn),比不上数组下标的O(1)。另外遍历到非s1中字符的时候,我的做法将当前记录的map清零,这个可能也比较费时。用vector模拟map的解法我偷懒没写,想看的就点进评论区看看吧。。。

 1 class Solution {
 2 public:
 3     bool checkInclusion(string s1, string s2) {
 4         if(s2.size()<s1.size()){
 5             return false;
 6         }
 7         int s2_siz=s2.size(),s1_siz=s1.size();
 8         int le=0,ri=0,cnt=0;
 9         map<char,int> dic;
10         for(char c:s1){
11             dic[c]+=1;
12         }
13         map<char,int>cur_dic;
14         while(ri<s2_siz){
15             char c=s2[ri++];
16             if(dic.count(c)){  //匹配
17                 ++cnt;
18                 cur_dic[c]++;
19                 while(dic[c]<cur_dic[c]){
20                     cur_dic[s2[le]]--;
21                     ++le;
22                     --cnt;
23                 }
24                 if(cnt==s1_siz){
25                     return true;
26                 }
27             }
28             else{   //不匹配
29                 cnt=0;
30                 cur_dic.clear();
31                 le=ri;
32             }
33         }
34         return false;
35     }
36 };

在这里插入图片描述

727. 最小窗口子序列

这题我一开始是这样思考的:要找S的最短子串满足T中字符按序存在于该子串内。那显然就不能像之前的题目那样用map了,因为此题多了出现顺序的要求。那么就先找第一个符合条件的子串再看看怎么继续下去。假如目前找到了0开始到i结束的子串包含T的子序列,那么从0开始的最短子串就是这个了。那下面把左边界右移一位再从1开始找?这样的复杂度不就是n^2了吗。然后我就卡在这了。
憋了半天还是去看评论区了,大致看到了两种解法:
第一种:评论链接和我的基本思路是一致的,但作者用了一种特别的优化。关键思路:如果当前我们找到了一个满足条件的子串,索引i到j,即i处对应T[0],j处对应T[-1]。
那么在该子串中满足条件的最短子串就是从右到左倒序依次查找T[-1],T[-2],T[-2]…到T[0]的子串(因为这个子串肯定不比j-i+1长,所以是最短的没毛病)。咋证明呢,很简单:对于i到j的串,因为最后一个元素是T[-1],而且这个元素只出现了这一次,那么最终的子串必须包含它。所以我们从末尾倒着往前查找T中的字符,最终找到的子串就一定是当前i到j串中满足条件的最短子串。比较遗憾的是,作者的代码格式不是很清晰,故我又自己写了一份在下面,思路完全参照作者。这个时间应该是最优O(n),最差O(n^2),比如S是100个字符a,T是aa,每次循环相当于只移动了一位。

 1 class Solution {
 2 public:
 3     string minWindow(string S, string T) {
 4         //找S的最短子串w,w中要包含T中的全部字符,并按顺序出现。
 5         if(S.size()<T.size()){
 6             return "";
 7         }
 8         int s_size=S.size(),t_size=T.size(),le=0,ri=0,cnt=0,_start=0,_end=INT32_MAX;
 9         while(ri<s_size){
10             if(S[ri]==T[cnt]){//匹配
11                 ++cnt;
12                 if(cnt==t_size){//找到符合条件的子串[le,ri]
13                     le=ri;
14                     while(cnt){//从ri倒序查找T[-1],T[-2]..到T[0]
15                         if(S[le]==T[cnt-1]){
16                             --cnt;
17                         }
18                         --le;
19                     }
20                     //此时cnt==0,S[le+1]==T[0]
21                     ++le;
22                     if(ri-le+1<_end-_start){
23                         _start=le;
24                         _end=ri+1;
25                     }
26                     ++le;
27                     ri=le-1;
28                 }
29                 ++ri;
30             }
31             else{
32                 ++ri;
33             }
34         }
35         return _end==INT32_MAX?"":S.substr(_start,_end-_start);
36     }
37 };

在这里插入图片描述
然后我又看到一种动态规划解法,tql,没想到。
思路是二维dp,dp[i][j]存储T截止到j的字符串t1,S截止到i包含t1子序列的左边界索引,这个时间是稳定的O(n^2)。

 1 class Solution {
 2 public:
 3     string minWindow(string S, string T) {
 4         if(S.size()<T.size()){
 5             return "";
 6         }
 7         int s_size=S.size(),t_size=T.size();
 8         vector<vector<int>>dp(t_size,vector<int>(s_size,-1));
 9         int le=0,ri=INT32_MAX;
10         //dp[i][j]存储S截止j的字符串s1,T截止i的字符串t1,s1包含t1的子序列的左索引
11         if(S[0]==T[0]){
12             dp[0][0]=0;
13         }
14         for(int i=1;i<s_size;++i){
15             if(S[i]==T[0]){
16                 dp[0][i]=i;
17             }
18             else if(dp[0][i-1]!=-1){
19                 dp[0][i]=dp[0][i-1];
20             }
21         }
22         for(int i=1;i<t_size;++i){
23             for(int j=i;j<s_size;++j){
24                 if(S[j]==T[i]){
25                     dp[i][j]=dp[i-1][j-1];
26                 }
27                 else{
28                     dp[i][j]=dp[i][j-1];
29                 }
30             }
31         }
32         // for(auto row:dp){
33         //     for(auto col:row){
34         //         cout<<col<<" ";
35         //     }cout<<endl;
36         // }
37         for(int i=t_size;i<s_size;++i){
38             if(dp[t_size-1][i]!=-1 and i-dp[t_size-1][i]<ri-le){
39                 le=dp[t_size-1][i];
40                 ri=i;
41             }
42         }
43         return ri==INT32_MAX?"":S.substr(le,ri-le+1);
44     }
45 };

在这里插入图片描述
完结撒花!2019年11月17日 00:46:57

猜你喜欢

转载自www.cnblogs.com/FdWzy/p/12288104.html