滑动窗口
目录
试题一:长度最小的子数组

算法原理
解法一:暴力枚举
暴力枚举出所有子数组的和
时间复杂度:O(N^3)
优化:在right遍历的同时,将结果加到sum中,可以省去求和遍历
时间复杂度:O(N^2)
解法二:滑动窗口
利用单调性,使用同向双指针来优化
left = 0,right = 0
right向右移动,进入窗口,计算窗口数据总和sum
sum小于target时,right++
sum大于等于target时,更新长度len,left++
更新窗口数据总和sum,再次判断,直到遍历完数组
时间复杂度:O(N)
本质:利用问题的特殊性质,砍掉重复的计算

代码编写
- 个人版本
cpp
class Solution
{
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int right = 0;
int left = 0;
int sum = 0;
int n = nums.size();
int len = 0;
while(right < n)
{
sum += nums[right];
while(sum >= target)
{
if(len == 0)
{
len = right - left + 1;
}
if(len > right - left + 1)
{
len = right - left + 1;
}
sum -= nums[left++];
}
right++;
}
return len;
}
};
- 标准版本
cpp
class Solution
{
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n = nums.size(),sum = 0,len = INT_MAX;
for(int left = 0,right = 0;right < n;right++)
{
sum += nums[right];//进窗口
while(sum >= target)//判断
{
len = min(len,right - left + 1);//更新结果
sum -= nums[left++];//出窗口
}
}
return len == INT_MAX ? 0 : len;
}
};
试题2:无重复字符的最长字串

算法原理
解法一:暴力枚举 + 哈希表(判断字符是否重复出现)
时间复杂度:O(N^2)
解法二:滑动窗口
left = 0,right = 0
进窗口:让字符进入哈希表
判断是否出窗口:窗口内出现重复字符时出窗口
从哈希表删除该字符,循环到窗口无重复字符
更新结果

代码编写
- 个人版本
cpp
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
unordered_set<char> hash;
int left = 0;
int right = 0;
int n = s.size();
int len = 0;
while(right < n)
{
while(hash.count(s[right]))
{
hash.erase(s[left]);
left++;
}
len = max(len,right - left + 1);
hash.insert(s[right++]);
}
return len;
}
};
- 标准版本
cpp
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
int hash[128] = {0};//使用数组模拟哈希表
int left = 0,right = 0,n = s.size();
int ret = 0;
while(right < n)
{
hash[s[right]]++;//进入窗口
while(hash[s[right]] > 1)//判断
hash[s[left++]]--;//出窗口
ret = max(ret,right - left + 1);//更新结果
right++;//让下一个元素进入窗口
}
return ret;
}
};
试题3:最大连续1的个数III

算法原理
转化为找出最长子数组,且0的个数不超过k个
解法一:暴力枚举
暴力枚举+zero计数器

解法二:滑动窗口
left = 0,right = 0
进窗口:如果right是1,right++;如果right是0,计数器加1,right++
判断是否出窗口:zero > k,left++,如果left是0,计数器减1
直到zero <= k ,窗口合法,窗口合法后更新结果
时间复杂度:O(N)
空间复杂度:O(1)

代码编写
- 个人版本
cpp
class Solution
{
public:
int longestOnes(vector<int>& nums, int k)
{
int left = 0;
int right = 0;
int zero = 0;
int num = 0;
int n = nums.size();
while(right < n)
{
if(nums[right] == 0)
{
zero++;
}
while(zero > k)
{
if(nums[left] == 0)
{
zero--;
}
left++;
}
num = max(num,right - left + 1);
right++;
}
return num;
}
};
- 标准版本
cpp
class Solution
{
public:
int longestOnes(vector<int>& nums, int k)
{
int ret = 0;
for(int left = 0,right = 0,zero = 0;right < nums.size();right++)
{
if(nums[right] == 0)//进窗口
{
zero++;
}
while(zero > k)//判断
{
if(nums[left++] == 0)//出窗口
{
zero--;
}
}
ret = max(ret,right - left + 1);//更新结果
}
return ret;
}
};
试题4:将x减到0的最小操作数

算法原理
正难则反:
转化为找出元素的和为sum - x(target)的最长(len)的子数组
n - len就是最小的操作数

解法:滑动窗口
left = 0,right = 0
进窗口:窗口值sum+=nums[right]
判断是否出窗口:当窗口值sum大于target,sum-=nums[left]出窗口
sum == target时更新结果
时间复杂度:O(N)

代码编写
- 个人版本
cpp
class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
int right = 0;
int left = 0;
int total = 0;
int ret = 0;
for(auto e : nums)
{
total += e;
}
int target = total - x;
int n = nums.size();
int sum = 0;
if(target < 0)
{
return -1;
}
if(target == 0)
{
return n;
}
while(right < n)
{
sum += nums[right];
while(sum > target)
{
sum -= nums[left];
left++;
}
if(sum == target)
{
ret = max(ret,right - left + 1);
}
right++;
}
return ret == 0 ? -1 : n - ret;
}
};
- 标准版本
cpp
class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
int sum = 0;
for(int a : nums) sum += a;
int target = sum - x;
//细节问题
if(target < 0) return -1;
int ret = -1;
for(int left = 0,right = 0,tmp = 0;right < nums.size();right++)
{
tmp += nums[right];//进窗口
while(tmp > target)//判断
tmp -= nums[left++];//出窗口
if(tmp == target)//更新结果
ret = max(ret,right - left + 1);
}
if(ret == -1) return ret;
else return nums.size() - ret;
}
};
试题5:水果成篮

算法原理
转化为找出一个最长的子数组长度,子数组中不超过两种类型的水果
解法一:暴力枚举+哈希表
通过哈希表判断水果种类是否超过两个

解法二:滑动窗口
left = 0,right = 0
进窗口: hash[f[right]]++
判断是否出窗口: 如果hash.size() > 2 ,hash[f[left]]--
更新结果

代码编写
- 个人版本
cpp
class Solution
{
public:
int totalFruit(vector<int>& fruits)
{
unordered_map<int,int> hash;
int left = 0,right = 0;
int sum = 0;
int n = fruits.size();
while(right < n)
{
hash[fruits[right]]++;
while(hash.size() > 2)
{
hash[fruits[left]]--;
if(hash[fruits[left]] == 0)
{
hash.erase(fruits[left]);
}
left++;
}
sum = max(sum,right - left + 1);
right++;
}
return sum;
}
};
- 标准版本
cpp
class Solution
{
public:
int totalFruit(vector<int>& f)
{
unordered_map<int,int> hash;//统计窗口内出现了多少种水果
int ret = 0;
for(int left = 0,right = 0; right < f.size(); right++)
{
hash[f[right]]++;//进窗口
while(hash.size() > 2)//判断
{
//出窗口
hash[f[left]]--;
if(hash[f[left]] == 0)
{
hash.erase(f[left]);
}
left++;
}
ret = max(ret,right - left + 1);
}
return ret;
}
};
- 优化版本
用数组模拟哈希表
cpp
class Solution
{
public:
int totalFruit(vector<int>& f)
{
int hash[1000001] = { 0 };//统计窗口内出现了多少种水果
int ret = 0;
for(int left = 0,right = 0,kinds = 0; right < f.size(); right++)
{
if(hash[f[right]] == 0)//维护水果的种类
{
kinds++;
}
hash[f[right]]++;//进窗口
while(kinds > 2)//判断
{
//出窗口
hash[f[left]]--;
if(hash[f[left]] == 0)
{
kinds--;
}
left++;
}
ret = max(ret,right - left + 1);
}
return ret;
}
};
试题6:找到字符串中所有字母异位词

算法原理
如何快速判断两个字符串是否是异位词
方法一:排序+比较:N*logN + N
方法二:利用哈希表,统计出现字符的次数

解法一:暴力枚举

解法二:滑动窗口+哈希表
left = 0,right = 0
进窗口:hash2[in]++
判断:right - left + 1 > m
出窗口:hash2[out]--
更新结果:check(hash1,hash2)
时间复杂度:26 * N → O(N)

优化:更新结果的判断条件
利用变量count来统计窗口中有效字符的个数
进窗口:进入后,hash2[in] ≤ hash1[in],count++
出窗口:出去前,hash2[out] ≤ hash1[out],count--
更新结果:count == m

代码编写
- 个人版本
cpp
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
vector<int> v;
int hash1[26] = {0};
int hash2[26] = {0};
int len = p.size();
int n = s.size();
int count = 0;
for(char ch : p)
{
hash1[ch - 'a']++;
}
int left = 0,right = 0;
while(right < n)
{
hash2[s[right] - 'a']++;
if(hash2[s[right] - 'a'] <= hash1[s[right] - 'a'])
{
count++;
}
if(right - left + 1 > len)
{
if(hash2[s[left] - 'a'] <= hash1[s[left] - 'a'])
{
count--;
}
hash2[s[left] - 'a']--;
left++;
}
if(count == len)
{
v.push_back(left);
}
right++;
}
return v;
}
};
- 标准版本
cpp
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
vector<int> ret;
int hash1[26] = {0};//统计字符串p中每个字符出现的个数
for(auto ch : p) hash1[ch - 'a']++;
int hash2[26] = {0};//统计窗口里面的每一个字符出现的个数
int m = p.size();
for(int left = 0,right = 0,count = 0;right < s.size();right++)
{
char in = s[right];
if(++hash2[in - 'a'] <= hash1[in - 'a']) count++;//进窗口 + 维护count
if(right - left + 1 > m)//判断
{
char out = s[left++];
if(hash2[out - 'a']-- <= hash1[out - 'a']) count--;//出窗口 + 维护count
}
//更新结果
if(count == m) ret.push_back(left);
}
return ret;
}
};
试题7:串联所有单词的字串

算法原理
解法一:滑动窗口+哈希表
哈希表:hash<string,int>
left与right指针的移动:移动的步长是每个单词的长度
滑动窗口执行的次数:单词的长度个数

代码编写
- 个人版本
cpp
class Solution
{
public:
vector<int> findSubstring(string s, vector<string>& words)
{
vector<int> ret;
int len = words[0].size();
int n = words.size();
int count = 0;
unordered_map<string,int> hash1,hash2;
for(auto e : words)
{
hash1[e]++;
}
for(int i = 0;i < len;i++)
{
int left = i,right = i;
count = 0;
hash2.clear();
while(right + len <= s.size())
{
string cur_right = s.substr(right,len);
hash2[cur_right]++;
if(hash2[cur_right] <= hash1[cur_right])
{
count++;
}
if(right - left + 1 > n * len)
{
string cur_left = s.substr(left,len);
if(hash2[cur_left] <= hash1[cur_left])
{
count--;
}
hash2[cur_left]--;
left += len;
}
if(count == n)
{
ret.push_back(left);
}
right += len;
}
}
return ret;
}
};
- 标准版本
cpp
class Solution
{
public:
vector<int> findSubstring(string s, vector<string>& words)
{
unordered_map<string,int> hash1;//保存words里面所有单词的频次
for(auto& s : words) hash1[s]++;
int len = words[0].size(),m = words.size();
vector<int> ret;
for(int i = 0;i < len;i++)//执行len次
{
unordered_map<string,int> hash2;//维护窗口内单词的频次
for(int left = i,right = i,count = 0;right + len <= s.size();right += len)
{
//进窗口+维护count
string in = s.substr(right,len);
hash2[in]++;
if(hash2[in] <= hash1[in]) count++;
//判断
if(right - left + 1 > len * m)
{
//出窗口+维护count
string out = s.substr(left,len);
if(hash2[out] <= hash1[out]) count--;
hash2[out]--;
left += len;
}
//更新结果
if(count == m)
{
ret.push_back(left);
}
}
}
return ret;
}
};
试题8:最小覆盖子串

算法原理
解法一:暴力枚举+哈希表

解法二:滑动窗口+哈希表
left = 0,right = 0
进窗口:hash2[in]++
判断:check(hash1,hash2)
更新结果:起始位置、最短长度
出窗口:hash2[out]--,直到判断不成立

优化:判断条件
使用变量count统计有效字符的种类
进窗口:进之后,当hash2[in] == hash1[in],count++
出窗口:出之前,当hash2[out] == hash1[out],count--
判断条件:count == hash1.size()
代码编写
- 标准版本
cpp
class Solution
{
public:
string minWindow(string s, string t)
{
int hash1[128] = {0};//统计字符串t中每个字符的频次
int kinds = 0;//统计有效字符的种类
for(auto ch : t)
{
if(hash1[ch] == 0)
{
kinds++;
}
hash1[ch]++;
}
int hash2[128] = {0};//统计窗口内每个字符的频次
int minlen = INT_MAX;
int begin = -1;
for(int left = 0,right = 0,count = 0;right < s.size();right++)
{
//进窗口
char in = s[right];
hash2[in]++;
//维护count
if(hash2[in] == hash1[in])
{
count++;
}
//判断条件
while(count == kinds)
{
//更新结果
if(right - left + 1 < minlen)
{
minlen = right - left + 1;
begin = left;
}
char out = s[left];
left++;
//维护count
if(hash2[out] == hash1[out])
{
count--;
}
//出窗口
hash2[out]--;
}
}
if(begin == -1)
{
return "";
}
else
{
return s.substr(begin,minlen);
}
}
};