滑动窗口
1.定义
不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
细节:对于窗口左右指针,是一个左闭右开的状态
2.滑动窗口通常用于以下两类问题:
- 固定大小的窗口问题
- 动态变化大小的窗口问题
2.1固定大小的窗口
对于固定大小的窗口,窗口的大小是固定的,每次移动窗口时,只需要移除窗口左边的元素并加入窗口右边的新元素。
示例问题:求数组中所有长度为 k 的子数组的最大和。
2.2动态大小的窗口
对于动态大小的窗口,窗口的大小可以变化,通常根据某些条件来调整窗口的左右边界。常用于满足特定条件的最小或最大子数组问题。
示例问题:求数组中和大于或等于 S 的最小子数组长度。(该数组全部是正数)
209. Minimum Size Subarray Sum
int minSubArrayLen(int target, vector<int>& nums) {
int j = 0;//j为左指针
int res = INT32_MAX;
int sum = 0;
int sublength = 0;
for(int i = 0;i<nums.size();i++){//i为右指针
sum+=nums[i];
while(sum >= target){
sublength = i - j + 1;
res = res < sublength ? res:sublength;
sum-=nums[j++];//左指针向左移动
}
}
return res == INT32_MAX ? 0 : res;
}
3. template
(from labuladong)
// 滑动窗口算法伪码框架
void slidingWindow(string s) {
// 用合适的数据结构记录窗口中的数据,根据具体场景变通
// 比如说,我想记录窗口中元素出现的次数,就用 map
// 如果我想记录窗口中的元素和,就可以只用一个 int
auto window = ...
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
window.add(c);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
window.remove(d);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
4.常见应用
4.1找到字符串中所有字母异位词:
438. Find All Anagrams in a String
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 异位词的子串,返回这些子串的起始索引。
vector<int> findAnagrams(string s, string p) {
unordered_map<char, int> need, window;
int left = 0, right = 0;
vector<int> res;
int valid = 0;
for(char c : p) need[c]++;
while(right < s.size()){
char a = s[right];
right++;
if(need.count(a)){
window[a]++;
if(need[a] == window[a]) valid++;
}
while(right - left == p.size()){
if(valid == need.size()) res.push_back(left);
char b = s[left];
left++;
if(need.count(b)){
if(need[b] == window[b]) valid--;
window[b]--;
}
}
}
return res;
}
30. 串联所有单词的子串 悬而未解
4.2最长无重复子串:
3. Longest Substring Without Repeating Characters
给定一个字符串,找出其中不含有重复字符的最长子串的长度。
int lengthOfLongestSubstring(string s) {
unordered_set<char> repeat;
int j = 0;//j为右指针
int res = 0;
for(int i = 0;i < s.size();i++){//i为左指针
if(i != 0) repeat.erase(s[i-1]);
while(j < s.size() && !repeat.count(s[j])){
repeat.insert(s[j]);
j++;
}
res = max(res, j - i);
}
return res;
}
4.3最小覆盖子串:
76. Minimum Window Substring
给定一个字符串 S 和一个字符串 T,找到 S 中最小的子串,使得 T 中的所有字符都在其中出现。
string minWindow(string s, string t) {
int left = 0, right = 0;
string str = "";
unordered_map<char, int> need, window;
for(char c:t) need[c]++;
//记录窗口中满足了need中的元素的长度
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while(right < s.size()){
// c 是将移入窗口的字符
char c = s[right];
// 增大窗口
right++;
if(need.count(c)){
window[c]++;
if(need[c] == window[c]) valid++;//保证增加后个数一致
}
// 进行窗口内数据的一系列更新
// 判断左侧窗口是否要收缩
while(valid == need.size()){
if(right - left < len){
start = left;
len = right - left;
}
char d = s[left];
left++;
if(need.count(d)){
if(need[d] == window[d]) valid--;//保证删除后的个数一致
window[d]--;
}
}
// 进行窗口内数据的一系列更新
}
return len == INT_MAX? "" : s.substr(start, len);
}
4.4字符串的排列(permutation):
567. Permutation in String
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,s1 的排列之一是 s2 的子串 。
bool checkInclusion(string s1, string s2) {
int left = 0, right = 0;
int valid = 0;
unordered_map<char, int> need, window;
for(char c : s1) need[c]++;
while(right < s2.size()){
char a = s2[right];
right++;
if(need.count(a)){
window[a]++;
if(need[a] == window[a]) valid++;
}
while(right - left == s1.size()){
if(valid == need.size()) return true;
char b = s2[left];
left++;
if(need.count(b)){
if(need[b] == window[b]) valid--;
window[b]--;
}
}
}
return false;
}
5.总结
滑动窗口技术通过移动窗口的左右边界来高效地解决很多数组和字符串问题。它避免了重复计算,通常可以将时间复杂度从 O(n^2) 降低到 O(n)。滑动窗口的核心在于如何正确移动窗口的左右边界,并根据问题的要求进行必要的计算。