本文记录作者刷题过程中与滑动窗口相关的题目。
1、连续子数组
674. 最长连续递增序列(简单难度)
子数组是原数组中连续的元素,中间不可以删除或添加其他元素,每个元素的相对顺序和原数组相同
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int len=nums.size();
if(len==1) return 1;
int res=0;// 最长长度
int left=0;
int right=1;
// 滑动窗口[left,right)表示递增区间,负责搜索数组中的所有递增区间
while(right<len){
// 如果新元素递增,扩展右边界
if(nums[right-1]<nums[right]){
right++;
}
// 如果新元素非递增,更新左边界,扩展右边界
else{
left=right;
right++;
}
res=max(res,right-left);//不断更新递增区间的最大长度
}
return res;
}
};
剑指 Offer 57 - II. 和为s的连续正数序列(简单难度)
class Solution {
public:
/*解析:本题是从正整数序列中查找所有和为target的连续子序列
难点在于每个连续子序列的长度、数量是不确定的。
这种类型题,特别是元素都是正整数,特别适合滑动窗口算法。
请注意,左指针和右指针的移动时机
当区间和>=target,右移左指针;当区间和<target,右移右指针
进阶:求解区间和的数学技巧:(left+right)*(right-left+1)/2
*/
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
int left=1;//正整数序列的第1个数
int right=2;//正整数序列的第2个数
int sum=0;
vector<int> sub;
// 滑动窗口[left,right]
while(left<right){
// 计算滑动窗口[left,right]的和
sum=0;
sub.clear();
for(int i=left;i<=right;i++){
sum+=i;
sub.push_back(i);
}
// 如果区间和==target,移动左指针
if(sum==target) {
res.push_back(sub);
left++;
}
// 如果区间和<target,右指针右移
else if(sum<target) right++;
// 如果区间和>target,左指针右移
else left++;
}
return res;
}
};
添加数学公式优化后:
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
int left=1;//正整数序列的第1个数
int right=2;//正整数序列的第2个数
int sum=0;
vector<int> sub;
// 滑动窗口[left,right]
while(left<right){
// 计算滑动窗口[left,right]的和
sum=(left+right)*(right-left+1)/2;
// 如果区间和==target,移动左指针
if(sum==target) {
sub.clear();
for(int i=left;i<=right;i++){
sub.push_back(i);
}
res.push_back(sub);
left++;
}
// 如果区间和<target,右指针右移
else if(sum<target) right++;
// 如果区间和>target,左指针右移
else left++;
}
return res;
}
};
209. 长度最小的子数组
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int minLength = INT32_MAX; // 求最小值就以整数的较大值初始化
int curlength = 0;//当前值
int start=0;//滑动窗口起始位置
int end=0;//滑动窗口终止位置
int sum=0;
//不满足滑动条件时就不断移动终止位置
for(int end=0;end<nums.size();end++){
sum+=nums[end];
//当满足条件时就不断移动起始位置
while(sum>=target){
curlength = end-start+1;//计算当前滑动窗口代表的值
minLength = minLength < curlength ? minLength : curlength;//注意:当前最小不一定就是全局最小
sum-=nums[start++];//滑动窗口起始位置后移
}
}
return minLength == INT32_MAX ? 0 : minLength;
}
};
904. 水果成篮
class Solution {
public:
int totalFruit1(vector<int>& tree) {
vector<int> count(tree.size());
int start = 0;//滑动窗口起始位置
int end =0;//滑动窗口终止位置
int dif = 0;//水果种类
for(end = 0; end < tree.size(); end++) {
if(count[tree[end]] == 0) {
count[tree[end]]++;//该种类水果数量+1
dif++;//种类+1
}
if(dif > 2) {
count[tree[start]]--;
if(count[tree[start]] == 0) {
//start++;
dif--;
}
start++;
}
}
return end - start;
}
/*这道题目可以理解为求只包含两种元素的最长连续子序列*/
int totalFruit(vector<int>& tree) {
int K = 2;
int end = 0, start = 0, res = 0;
unordered_map<int, int> dict;
for (end = 0; end < tree.size(); end++) {
dict[tree[end]]++;//该种类水果数量增加
//当不符合滑动窗口条件时
while(dict.size() > K)
{
res = max(res, end - start);
dict[tree[start]]--;
if(dict[tree[start]] == 0) {
dict.erase(tree[start]);
}
start++;
}
}
res = max(res, end - start);
return res;
}
};
3. 无重复字符的最长子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char,int> dict;// <字符,频率>
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int right=-1;
int res=0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for(int left=0;left<s.length();++left){
if(left!=0) dict.erase(s[left-1]);
// 当右指针下一步没有到达数组右边界且右指针下一步没有重复,不断地移动右指针
while(right+1<s.length()&&!dict.count(s[right+1])){
dict[s[right+1]]++;
right++;
}
// 第 left 到 right 个字符是一个最长的无重复字符子串
res = max(res, right - left + 1);
}
return res;
}
};
567. 字符串的排列
剑指 Offer II 014. 字符串中的变位词
class Solution {
public:
/*由于排列不会改变字符串中每个字符的个数,
所以只有当两个字符串每个字符的个数均相等时,
一个字符串才是另一个字符串的排列。
记 s1的长度为n,我们可以遍历2中的每个长度为n 的子串,
判断子串和s1中每个字符的个数是否相等,
若相等则说明该子串是s1的一个排列*/
bool checkInclusion(string s1, string s2) {
int n = s1.length(), m = s2.length();
if (n > m) return false;
vector<int> cnt1(26), cnt2(26);
for (int i = 0; i<n; ++i) {
++cnt1[s1[i]-'a'];
++cnt2[s2[i]-'a'];
}
if (cnt1 == cnt2) return true;
for (int i = n; i < m; ++i) {
++cnt2[s2[i]-'a'];
--cnt2[s2[i -n]-'a'];
if (cnt1 == cnt2) return true;
}
return false;
}
};
438. 找到字符串中所有字母异位词
class Solution {
public:
/**时间复杂度O(N),空间复杂度O(1); 技术:滑动窗口+计数索引
一、计数索引,两个串中的字母只有小写英文字母,用大小为26的数组存放字母频率
二、滑动窗口[left,right]
在s中移动一个滑动窗口,如果滑动窗口内的字母频率>=t的字母频率,则滑动窗口符合条件
移动滑动窗口时,需要时刻维护滑动窗口内的所有字母频率sFreq
left的移动范围是[0,s_len-t_len],right的移动范围是[-1,s_len-1]
如何移动滑动窗口呢?默认先移动right,比较winSize和t.size(),判断是否移动left:
1) 当winSize < t.size() 移动right;// 此时滑动窗口一定不符合要求
2) 当winSize == t.size() 并且符合要求时: 添加left到结果中,移动left
3) 当不符合要求时: 移动left*/
vector<int> findAnagrams(string s, string p) {
vector<int> res;
int s_len=s.length();
int p_len=p.length();
if(s_len<p_len) return res;
// 统计p的字母频率
vector<int> pFec(26,0);
for(char c:p) pFec[c-'a']++;
// 滑动窗口
int left=0;// 遍历范围为[0,s_len-p_len]
int right=-1;// 遍历范围为[-1,s_len-1]
vector<int> winFec(26,0);
while(left<=s_len-p_len){
int winSize=right-left+1;
// 如果滑动窗口尺寸较小,移动right
if(winSize<p_len){
right++;
winFec[s[right]-'a']++;
}
// 如果滑动窗口尺寸正好,检查要求,移动left缩小
else if(winSize==p_len&&check(winFec,pFec)){
res.push_back(left); // 符合条件
winFec[s[left++]-'a']--; // 缩小窗口
}
// 如果滑动窗口尺寸较大,移动left缩小
else winFec[s[left++]-'a']--; // 缩小窗口
}
return res;
}
bool check(vector<int>& sFec,vector<int>& pFec){
for(int i=0;i<26;++i) if(sFec[i]!=pFec[i]) return false;
return true;
}
};
76. 最小覆盖子串
class Solution {
public:
/**时间复杂度O(N),空间复杂度O(1); 技术:滑动窗口+计数索引
一、计数索引
该题目的两个串中的字母只有大小写英文字母,可以开辟一个大小为64的数组存放字母频率
通过字母的ASCII码作为数组的索引,开辟空间的大小为26+6+26=58:26个大写字母,26个小写字母,
还有中间的6个非字母 A~Z[65~90] 非字母[91~96] a~z[97~122]
二、滑动窗口[left,right]
在s中移动一个滑动窗口,如果滑动窗口内的字母频率>=t的字母频率,则滑动窗口符合条件
移动滑动窗口时,需要时刻维护滑动窗口内的所有字母频率sFreq
left的移动范围是[0,s_len-t_len],right的移动范围是[-1,s_len-1]
如何移动滑动窗口呢?默认先移动right,比较winSize和t.size(),判断是否移动left:
1) 当winSize < t.size() 移动right;// 此时滑动窗口一定不符合要求
2) 当winSize >= t.size() :
2.1) 如果滑动窗口符合要求:
2.1.1)winSize == t.size()时,一定是最小覆盖子串,直接return
2.1.2) winSize > t.size()时,保存left和right,尝试移动left
2.2) 如果滑动窗口不符合要求:
2.2.1)如果right移动到头,移动left
2.2.2) 如果right没有移动到头,移动right
*/
string minWindow(string s, string t) {
int t_len=t.size();
int s_len=s.size();
if (s_len<t_len) return "";
// 统计t字符串中每个字母的出现频率
vector<int> tFreq(64,0);// t字母串的所有字母频率
for (int i=0; i<t_len; i++) tFreq[t[i]-'A']++;
// 滑动窗口为[left,right],窗口大小是winSize=right-left+1
int left = 0, right = -1;
vector<int> winFreq(64,0);// 滑动窗口的所有字母频率
int edge[2]={-1,s_len+1}; // 保存的最小覆盖子串的左右边界
while (left <= s_len-t_len) {
int winSize=right-left+1;
//窗口较小时,移动right
if(winSize<t_len) {
//if (right+1>=s_len) break;// 防止越界
right++;//移动右边界
winFreq[s[right]-'A']++;
}
// 如果符合要求
else if(check(winFreq,tFreq)){
// 如果窗口大小==t的长度,一定是最小覆盖子串
if (winSize == t.size()) return string(s.begin()+left, s.begin()+right+1);
// 如果窗口大小>t的长度,不一定是最小覆盖子串,可能要保存,移动left
else {
// 如果比上个保存子串小,则更新
if (right-left<edge[1]-edge[0]) {
edge[0] = left;
edge[1] = right;
}
winFreq[s[left]-'A']--;
left++;
}
}
// 如果不符合要求,移动right
else{
// 如果right没有越界,移动right
if (right+1<s_len) {
right++;
winFreq[s[right]-'A']++;
}
// 如果right已经到头,移动left
else{
winFreq[s[left]-'A']--;
left++;
}
}
}
return edge[0] == -1 ? "" : string(s.begin() + edge[0], s.begin() + edge[1] + 1);
}
// 检查此时窗口中的字母频率是否和t的频率一致
bool check(vector<int>& winFreq, vector<int>& tFreq){
for(int i = 0;i < 64;i++){
if (winFreq[i]<tFreq[i]) return false;
}
return true;
}
};