滑动窗口问题
滑动窗口
1、滑动窗口3问题
- 1、如何向窗口中添加新元素?
- 2、如何缩小窗口,也就是窗口减数?
- 3、在窗口滑动的哪个阶段更新结果?
int left = 0, right = 0;
while (right < s.size()) {
// 增大窗口
window.add(s[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
}
}
2、题目
2.1、滑动窗口最大值(最纯粹的窗口算法)
例如,数组为【4,3,5,4,3,3,6,7】,窗口大小为3时:
【4,3,5】,4,3,3,6,7 窗口中最大值为5
4,【3,5,4】,3,3,6,7 窗口中最大值为5
4,3,【5,4,3】,3,6,7 窗口中最大值为5
4,3,5,【4,3,3】,6,7 窗口中最大值为4
4,3,5,4,【3,3,6】,7 窗口中最大值为6
4,3,5,4,3,【3,6,7】 窗口中最大值为7
2.1.1、思路
窗口如下:
- 1、L与R之间的数就是窗口内的数
- 2、L和R的初始位置为数组的左边
- 3、L和R都只能右移,不可后退;且L不可超过R
核心是利用双端队列
结构(R移动一种情况,L移动一种情况),内部存放数组arr的index
位置信息。双端队列的头节点就是当前窗口的最大值。要求内部: 大— — — —小
排列.
双向队列:既可以从头部弹出,也可以从尾部弹出的队列结构
具体操作如下:
【1】right 右滑,窗口加数:
- 1)如果
queue为空
,直接把下标i
放入queue
中; - 2)如果
queue不为空
,取出当前queue队尾
存放的下标 j
。如果arr[j] > arr[i]
,则直接把i 放入队尾
; - 3)如果
arr[j] <= arr[i]
,则一直从queue 的队尾
弹出下标,直到某个下标在queue中的对应值大于 arr[i]
,然后把i
放入队尾
【为什么可以弹出,因为我永远比你晚过期,我又比你大或者和你一样大,有我在,你永远不可能最大,所以你可以滚了】
【2】left 右滑,窗口减数:
- 1)看弹出的
left 是否与队列头相等
,如果相等,说明这个队列头已经不在窗口内了,所以弹出 queue
当前的队首元素,这就是为什么要保存数组下标的原因。
【3】哪个阶段更新结果:
当数组索引i大于窗口大小的情况下都可以,也就是i >= w - 1
,然后从双端队列首部pop_front().
2.1.2、题解
vector<int> maxValuesInWindows(int arr[], int n, int w) {
vector<int> res;
deque<int> qmax;//双向队列
for (int i = 0; i < n; i++){
while (!qmax.empty() && arr[qmax.back()] < arr[i]) {//加数
qmax.pop_back();
}
qmax.push_back(i);
if (qmax.front() == i - w) {//减数
qmax.pop_front();
}
if (i >= w - 1) {//更新
res.push_back(qmax.front());
}
}
return res;
}
2.2、leetcode76、最小覆盖子串(往下都是基于滑动窗口思想)
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
提示:
如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
2.2.1、思路
首先提出基于滑动窗口思想
的模板框架
/* 滑动窗口算法框架 */
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++;
// 进行窗口内数据的一系列更新
...
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
滑动窗口算法的思路是这样:
1、我们在字符串S中使用双指针中的左右指针技巧,初始化left = right = 0,把索引左闭右开区间[left, right)称为一个「窗口」。
2、我们先不断地增加right指针扩大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)。
3、此时,我们停止增加right,转而不断增加left指针缩小窗口[left, right),直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同时,每次增加left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到right到达字符串S的尽头。
下面画图理解一下,needs和window相当于计数器,分别记录T中字符出现次数和「窗口」中的相应字符的出现次数。
1、初始状态
增加right,直到窗口[left, right)包含了T中所有字符:
现在开始增加left,缩小窗口[left, right)。
4、直到窗口中的字符串不再符合要求,left不再继续移动。
之后重复上述过程,先移动right,再移动left…… 直到right指针到达字符串S的末端,算法结束。
现在开始套模板,只需要思考以下四个问题:
1、当移动right扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?
3、当移动left缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加window计数器;如果一个字符将移出窗口的时候,应该减少window计数器;当valid满足need时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
2.2.2、题解
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
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 (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ? "" : s.substr(start, len);
}
2.3、leetcode 567 字符串排列
原题链接
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
注意:
输入的字符串只包含小写字母
两个字符串的长度都在 [1, 10,000] 之间
2.3.1、思路
明显的滑动窗口算法,相当给你一个S和一个T,请问你S中是否存在一个子串,包含T中所有字符且不包含其他字符?
1、本题移动left缩小窗口的时机是窗口大小大于t.size()时,因为排列嘛,显然长度应该是一样的。
2、当发现valid == need.size()时,就说明窗口中就是一个合法的排列,所以立即返回true。
至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
2.3.2、题解
// 判断 s 中是否存在 t 的排列
bool checkInclusion(string t, string s) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (right - left >= t.size()) {
// 在这里判断是否找到了合法的子串
if (valid == need.size())
return true;
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 未找到符合条件的子串
return false;
}
2.4、leetcode 438 找所有字母异位词
原题链接
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。 不考虑答案输出的顺序。
示例 1:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
示例 2:
示例 2:
输入:
s: "abab" p: "ab"
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。
2.4.1、思路
这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?相当于,输入一个串S,一个串T,找到S中所有T的排列,返回它们的起始索引。
2.4.2、题解
vector<int> findAnagrams(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
vector<int> res; // 记录结果
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (right - left >= t.size()) {
// 当窗口符合条件时,把起始索引加入 res
if (valid == need.size())
res.push_back(left);
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
return res;
}
2.5、leetcode 3 最长无重复子串
题目链接
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
2.5.1、题解
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
window[c]++;
// 判断左侧窗口是否要收缩
while (window[c] > 1) {
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
window[d]--;
}
// 在这里更新答案
res = max(res, right - left);
}
return res;
}
这就是变简单了,连need和valid都不需要,而且更新窗口内数据也只需要简单的更新计数器window即可。
当window[c]值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left缩小窗口了嘛。
唯一需要注意的是,在哪里更新结果res呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
这里和之前不一样,要在收缩窗口完成后更新res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
参考
1、https://blog.csdn.net/qianji_little_boy/article/details/83591712
2、https://mp.weixin.qq.com/s/ioKXTMZufDECBUwRRp3zaA