滑动窗口
字符串类的滑动窗口
leetcode 76. 最小覆盖子串
一般的题目: 有一个子串与一个主串, 找到这个串的起始地址、这个串是否存在、子串在主串是否存在并找出最小的串。
滑动窗口的思路:维护一个窗口,不断滑动,然后更新答案么,那么如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果呢? 细节问题很烦人; 首先初始化左右窗口的值都是零(窗口就是双指针的另外一种表现形式),根据需要进行窗口的更新;
leetcode 76: 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
比如这题: 最简单的方法是使用二层for循环,第一层确定 i 的位置, 第二层从 i 的位置往后查找是否包含 T 所有的字符,一旦找到了T,就立马退出的二层循环,跟新答案;
你有没有发现:
当 i = 0 时, 会找到” ADOBEC“ 这个串包含 T 所有字符; 答案为 ” ADOBEC“
当 i = 1时, 会找到” DOBECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 2时, 会找到” OBECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 3时, 会找到” BECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 4时, 会找到” ECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 5时, 会找到” CODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
- 1
- 2
- 3
- 4
- 5
- 6
第 1 ~ 5 步: 第 1 步找到的串, 包含后面 2 ~ 5步找到的串, 所以第2 ~ 5步的努力都是白费力气, 白做了, 如果这个过程可以优化掉,偷偷懒,那么时间复杂度就会降低不少
当 i = 6时, 会找到” ODEBANC“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 7时, 会找到” DEBANC“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
当 i = 8时, 会找到” EBANC“ 这个串包含 T 所有字符; 答案跟新为 ” EBANC“
当 i = 9时, 会找到” BANC“ 这个串包含 T 所有字符; 答案跟新为 ” BANC“
- 1
- 2
- 3
- 4
第 6 ~ 9 步:与前面情况一样, 这里也可以优化;
优化策略就是加入两个指针(i , j), 称为滑动窗口;
两个指针首先指向0: 首先 j 先走(往右),直到在窗口的范围类(i,j)的串包含"BANC"; 然后 i 走(往右),直到在窗口的范围类(i,j)的串不包含"BANC"; 这时候跟新答案,答案字符串为窗口的范围覆盖的字符串; 简而言之,j 找符合条件的字符串,i 破环字符串直到不符合条件。
找符合条件的字符串,可以用散列表加一个 数量 nums(为T的长度) ,每找到一个字符nums就自减,等于零就是找到了; 破环字符串反过来,每找到一个字符nums就自加,nums大于零 字符串就被破环了;
这一题有个细节就是小写字母与大小写字母都有, 所以散列表要开255;
AC代码:
class Solution {
public:
string minWindow(string s, string t) {
int m[255]; //开散列表
int i, j, nums; // i , j 双指针; nums : 长度
string outc = ""; //答案字符串
memset(m, 0, sizeof(m)); //初始化散列表每一个元素为零
for (auto ch : t) m[ch - 'A']++; //对应的字母位的数量+1
nums = t.size(); //复制t的长度
i = j = 0; //复制为 0
while (true) {
while (nums != 0 && j < s.size()) { //找符合条件的字符串,nums == 0 就是找到了
if(m[s[j] - 'A'] > 0) //判断s[j]这个字符是否已经被找到了, 如果已经被找到了就不需要nums--了
nums--;
m[s[j] - 'A']--;//减少s[j] 这个字母的数量
j++;
}
if (nums != 0) break;
while (nums == 0 && i < j) {//破环字符串,nums > 0 就表示字符串被破坏了
m[s[i] - 'A']++; //将s[i]这个字符的数量+1
if(m[s[i] - 'A'] > 0) //如果s[i]这个字符的数量大于零,说明已经破环了字符串, nums 加 1;
nums++;
i++;
}
//更新答案字符串窗口的范围覆盖的字符串
if (outc.size() == 0) outc = s.substr(i - 1, j - i + 1); //第一次跟新字符串;
outc = j - i < outc.size()? s.substr(i - 1, j - i + 1) : outc; //比之前的字符串短才跟新
}
return outc;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
接下来的 leetcode 567、438 都可以套用这个模板写;
leetcode 567. 字符串的排列
class Solution { public: bool checkInclusion(string s1, string s2) { if (s1.size() == 0) return true; int m[255]; int i, j, nums; memset(m, 0, sizeof(m)); for (auto ch : s1) m[ch - 'A']++; nums = s1.size(); i = j = 0; while (true) { while (nums != 0 && j < s2.size()) { if (m[s2[j] - 'A'] > 0) nums--; m[s2[j] - 'A']--; j++; } if (nums != 0) break; while (nums == 0 && i < j) { m[s2[i] - 'A']++; if (m[s2[i] - 'A'] > 0) nums++; i++; } //更新答案字符串窗口的范围覆盖的字符串 //if (outc.size() == 0) outc = s.substr(i - 1, j - i + 1); //第一次跟新字符串; //outc = j - i < outc.size()? s.substr(i - 1, j - i + 1) : outc; //比之前的字符串短才跟新 //上一题的这个地方的代码
<span class="token comment">//这题修改后的代码</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>j <span class="token operator">-</span> i <span class="token operator">+</span> <span class="token number">1</span> <span class="token operator">==</span> s1<span class="token punctuation">.</span><span class="token function">size</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
leetcode 438. 找到字符串中所有字母异位词
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
if (p.size() == 0) return {};
int m[255];
int i, j, nums;
vector<int> outc;
memset(m, 0, sizeof(m));
for (auto ch : p) m[ch - 'A']++;
nums = p.size();
i = j = 0;
while (true) {
while (nums != 0 && j < s.size()) {
if (m[s[j] - 'A'] > 0)
nums--;
m[s[j] - 'A']--;
j++;
}
if (nums != 0) break;
while (nums == 0 && i < j) {
m[s[i] - 'A']++;
if (m[s[i] - 'A'] > 0)
nums++;
i++;
}
//相对于上一题, 只是将答案插入数组中;
if (j - i + 1 == p.size()) outc.push_back(i - 1);
}
return outc;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
leetcode 3. 无重复字符的最长子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.erase(s[i - 1]);
}
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 不断地移动右指针
occ.insert(s[rk + 1]);
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
数值类的滑动窗口
leetcode 1438. 绝对差不超过限制的最长连续子数组
这题就是正常滑动窗口的思路, 但我们需要维护一个最大值与最小值, 确保这个最大值与最小值在窗口中;
j 往右滑动,找到满足条件的最大的窗口(绝对值小于等于限制值就一直往右滑动,大于就等下); j 不动了, 说明窗口中的绝对值大于限制值了。
i 往右滑动(重新找到一个满足条件的)窗口, 找到满足条件的最大的窗口(绝对值大于限制值就一直往右滑动,小于或等于就停下)。
先 j 动, 后 i 动。
这里我们维护一个最大值与最小值, 首先想到的是排序, 但之后我们又要删掉出窗口的值,就得查找 跟 删除, 这样我们就可以使用set集合,set 自身实现排序 并 自带 查找与删除。 set会自动去重, 选用 multiset;
class Solution {
public:
int longestSubarray(vector<int>& nums, int limit) {
int i = 0, j = 0, len = 1;
multiset<int> s;
while (j < nums.size()) {
//j向右扩大窗口
//绝对值的差小于 limit 就可以一直扩大窗口
while (j < nums.size() && (s.empty() || abs(*(s.begin()) - *(s.rbegin())) <= limit)) {
s.insert(nums[j++]);
}
//右边界的处理
if (abs(*(s.begin()) - *(s.rbegin())) <= limit) len = max(len, j - i);
else len = max(len, j - i - 1);
//i 缩小窗口.
//绝对值的差大于 limit 就停止缩小窗口
while (i < j && abs(*(s.begin()) - *(s.rbegin())) > limit) {
auto it = s.find(nums[i++]);
if (it != s.end()) {
s.erase(it);
}
}
}
return len;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
leetcode 239. 滑动窗口最大值
这题用固定窗口写, 只需维护一个最大值就好了, 可以用map<int,int,greater<>>;
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
map<int, int, greater<int>> m;
vector<int> v;
for (int i = 0; i < k; i++)
m[nums[i]]++;
for (int i = 0, j = k - 1; j < nums.size(); ) {
v.push_back(m.begin()->first);
m[nums[i]]--;
if (m[nums[i]] == 0) m.erase(nums[i]);
i++;
j++;
if(j < nums.size())m[nums[j]]++;
}
return v;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
总结:
双指针类的滑动窗口问题思维复杂度并不高,但是出错点往往在细节。记忆常用的解题模版还是很有必要的,特别是对于这种变量名多,容易混淆的题型。有了这个框架,思考的点就转化为 “什么条件下移动左指针”,无关信息少了,思考加实现自然不是问题。