目录
滑动窗口原理
真懂了滑动窗口?
滑动 + 字符串细节
开干切题
滑动窗口原理
滑动窗口:维护一前一后两根指针, 或者说一左一右两个指针。更主要的是维护左右指针中的区间. 同时不断的向前滑动,直到整个序列滑动结束,前指针走到序列末尾,所有结果得到收集。
分清职责:
区间维护(题目要求):restriction (限制),限制窗口中的元素。不是什么元素都要维护在窗口中的。必须符合要求,不是所有的水都叫做百岁山。
前指针(探路兵):主打一个铁憨憨,一生的唯一使命就是扩大窗口, 同时维护一个计数器或者是题目要求序列的一个积累判断。 核心:前指针,积累(符合题干的)序列,扩大区间。
后指针(区间头):主打一个收拢区间,收集结果。后指针,按道理应该始终指向(满足题干要求)序列的头部,收集结果。
怎么让后指针(区间的头)走到(符合要求)序列头部?是我们需要考虑的问题。也是一个随意写滑动窗口Bug频出的问题?
如果,兄弟们可以看懂上述理论,那接下来完全有必要可以再瞅瞅,如果看不懂理论,下面还有滑动窗口题目的诸多细节也可以学习。究其根本,滑动窗口也仅仅只是一种思维而已,具体如何实现,各有所好,上述也仅仅只是提供一种思维角度。题目做得慢,bug频出,很多时候是由于各位兄弟蒙没有去自己总结这样的套路。虽然,编程讲究随意性,大佬不管怎么写都没问题,但是人家哪些杀出来的,真不是咱这点小训练可比的,最好就是一周周,勤劳的一个一个小点的攻克,攻克完了总结出自己的切题模板,套路出来
真懂了滑动窗口?
提及滑动窗口, 这仿佛是刷题人必经之路, 人尽皆知的算法了。上面的懂了,是不是刷滑动窗口题目就很顺利了吗?那必然不是,鹅鹅鹅,为啥,上面的模板不是已经挺成型的吗?
NONONO. 滑动窗口题目除了核心整体考滑动窗口,他还可以结合字符串的知识点一起考核。
----- 而不同的字符串匹配规则,对应着不同的处理措施。
滑动 + 字符串细节
字符串基础:
1. 子串 和 子序列的区别
子串: 连续子串 原串: abcdgf 目标串: abc 匹配子串: abc
子序列: 子串中可以多出字符 原串: abcdgf 目标串: acd 匹配子序列: abcd
2. 子串的排列 和 子序列的区别.
排列:包含所要求字符即可, 顺序可变 (应对措施,化变为不变,用位置映射消除顺序之别,map,set,或是vector均可.) 比如 原串: acbdgh 目标串: [abc] 匹配子序列: acb. (包含acb字符即可) 很明显利用容器 收集路径字符即可。容器自然而然的消除了字符位置之间的顺序性。
子序列和子串, 顺序都不可变。很明显,指针顺序遍历即可,按照原序列遍历,具备顺序性。
各有各的玩法, 各有各的规则和处理办法.
滑动窗口基础:
1. 指针一前一后, 维护一个区间, 覆盖前进. 前指针一直向后试探, 慢指针根据题目要求维护窗口(区间大小)
2. 窗口滑动的过程中收集结果. 得出ans
开干切题
光说不练,纸上谈兵是无效的。所以刚说的那么多还是需要应用于实际的代码中.
无重复字符的最长子串
力扣
题目要求很明显了,不可以包含重复字符,采取set作为存储窗口区间元素可以,采取数组映射也可以。(维护区间越长越棒棒哒,唯一要求,不可出现重复,出现重复就需要收集结果,收缩区间。)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ans = 0;
unordered_set<int> dict;
int last = 0, front;
for (int front = 0; front < s.size(); front++) {
char ch = s[front]; //放入窗口的元素.
while (dict.find(ch) != dict.end()) { //出现重复, 需要收缩区间.
char delch = s[last];
dict.erase(delch);
last ++;
}
dict.insert(ch);
ans = max(ans, front - last + 1);//获取ans
}
return ans;
}
};
字符串的排列 (无序性 + vector数组映射 + 元素不变性) 排列: 顺序可以打乱. 元素不变性字符不变, 不可多或少
力扣
字符串的排列,很明显,可以直接利用数组映射来处理。什么意思,这个题目仅仅包含小写字符。所以,可以直接使用26的数组映射就OK了。字符的排列,说白了与顺序无关,仅仅与数目有关系,只要对应字符数目满足了就OK了,还有一点,排列,不能包含其他字符,所以其他字符出现明显,需要upset整个区间
/*
dict: 模板数组,记录着s1串中的信息
count: 记录着滑动窗口滑动过程中的路径信息.
needcnt: 需要满足要求的的字符数目
cnt: 当前满足要求的字符数目
*/
class Solution {
public:
bool checkInclusion(string s1, string s2) {
std::vector<int> dict(26, 0), count(26, 0);
int last = 0, front, needcnt = 0, cnt = 0;
for (auto & e : s1) {
needcnt += dict[e-'a'] == 0;
dict[e-'a'] ++;
}
for (front = 0; front < s2.size(); front ++) {
char ch = s2[front];//即将入滑动窗口字符.
if (!dict[ch-'a']) {//ch不在要求字符中, 卒, 断了, 一切重置.
count.assign(26, 0);
last = front + 1;
cnt = 0;
continue;
}
count[ch-'a'] ++;//放进来.
if (count[ch-'a'] == dict[ch-'a']) cnt ++;
if (needcnt == cnt) {//出现ans
return true;
}
while (front - last + 1 >= s1.size()) {//超出区间长度.
char delch = s2[last];
cnt -= (count[delch-'a'] == dict[delch-'a']);
count[delch-'a'] --;
last ++;
}
}
return false;
}
};
找到字符串中所有字母异位词 (子序列问题:顺序性 + 元素可变性) 元素可变性, 中间可以插入
力扣
很明显,是上一道题目的一个升级版本. 几乎一毛一样,起码套路是一样滴。而且这个更好写. 收集答案的时候进行收缩就OK了.
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> dict(26, 0), count(26, 0), ans;
int last = 0, front, cnt = 0, needcnt = 0;
for (auto &e : p) {
needcnt += dict[e-'a'] == 0;
dict[e-'a'] += 1;
}
for (front = 0; front < s.size(); front ++) {
char ch = s[front];
if (!dict[ch-'a']) { //序列打断, 全体重置.
cnt = 0;
count.assign(26, 0);
last = front + 1; continue;
}
count[ch-'a'] ++;
cnt += count[ch-'a'] == dict[ch-'a'];
while (front - last + 1 >= p.size()) {//收缩窗口, 收集ans
if (cnt == needcnt) ans.push_back(last);
char delch = s[last];
cnt -= (count[delch-'a'] == dict[delch-'a']);
count[delch-'a'] --;
last ++;
}
}
return ans;
}
};
力扣
class Solution {
public:
string findLongestWord(string s, vector<string>& dict) {
int n = s.size();
std::sort(dict.begin(), dict.end(), [](string& s1, string& s2) -> bool {
if (s1.size() > s2.size()) return true;
if (s1.size() < s2.size()) return false;
return s1 < s2;
});
int last = 0, front = 0;
for (int i = 0; i < dict.size(); i++) {//遍历所有的dict寻找ans
for (front = 0; front < s.size(); front++) {
if (s[front] == dict[i][last]) {
last ++;
}
if (last >= dict[i].size()) {
return dict[i];
}
}
last = 0;//从新开始找下一个。
}
return "";
}
};
思路:话不多说,这个题目,准确来说,不算是滑动窗口。但是也利用了双指针的技巧。先翻译题意,删除字符得到匹配,说白了就是中间存在多余字符,但是顺序性还在,明显的子序列匹配问题。
子序列匹配,待匹配串和匹配串各一个指针同时走,待匹配串的指针走到末尾即为匹配成功。此题要求匹配尽量长,一样长按照字典序,为了尽量少匹配,所以可以先按照规则排序,再依次遍历寻找最优解.
字符串中的所有变位词
力扣
重复了,上面的异位词一样的.
最小覆盖子串
典型的滑动窗口,如果搞懂了上述理论直接套用模板解决.
https://leetcode.cn/problems/minimum-window-substring/description/
class Solution {
public:
string minWindow(string s, string t) {
int cnt = 0, needcnt = 0, start = 0, minlen = INT_MAX;
std::vector<int> dict(128), count(128);
for (auto &e : t) {//记录目标字符
needcnt += (dict[e] == 0); //需要满足要求的字符数目.
dict[e] ++;
}
int last = 0, front = 0;
for (; front < s.size(); front++) {
char ch = s[front];
if (dict[ch]) { //是其中的元素.
count[ch] ++;//后续多出来的也无所谓.
cnt += (count[ch] == dict[ch]);//满足字符要求
}
while (cnt == needcnt) {//ans
if ((front - last + 1 < minlen)) {//跟新ans
start = last;
minlen = front - last + 1;
}
//尝试缩小窗口
char delch = s[last];
last ++;
if (dict[delch]) {
cnt -= dict[delch] == count[delch];
count[delch] --;
}
}
}
return minlen == INT_MAX ? "" : s.substr(start, minlen);
}
};
重复的DNA序列
https://leetcode.cn/problems/repeated-dna-sequences/
class Solution {
public:
vector<string> findRepeatedDnaSequences(string s) {
int last = 0, front;
vector<string> ans;
unordered_map<string, int> dict;
for (front = 0; front < s.size(); front ++) {
if (front - last + 1 == 10) {
//maybe ans
string tmp = s.substr(last, 10);
if (dict.find(tmp) != dict.end()) {
if (dict[tmp] == 1) ans.push_back(tmp);
}
dict[tmp] ++;
last ++;//剔除元素, 缩小窗口
}
}
return ans;
}
};
最长公共前缀
https://leetcode.cn/problems/longest-common-prefix/
class Solution {
public:
//遍历走
string longestCommonPrefix(vector<string>& strs) {
char ch;
int ind = 0;
while (1) {
for (int i = 0; i < strs.size(); i++) {
if (ind >= strs[i].size()) return strs[i];
ch = (i == 0 ? strs[i][ind] : ch);
if (ch != strs[i][ind]) {
return ind == 0 ? "" : strs[i].substr(0, ind);
}
}
ind ++;
}
}
};
滑动窗口的最大值
https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/
此题也算是一道经典的复合题目,其中还包含了单调栈或者说单调队列的一点单调序列的思维在里面,这个很经典,如果后续又继续希望可以跟大家分享。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//维护单调队列吧
std::deque<int> less_que;//单调递减队列
vector<int> ans;
int last = 0, front = 0, need = INT_MIN;//need记录需要值
for (; front < nums.size(); front ++) {
while (!less_que.empty() && nums[front] > less_que.back()) {
less_que.pop_back();
}
less_que.push_back(nums[front]);
if (front - last + 1 == k) {//收集结果, 缩小区间
ans.push_back(less_que.front());
if (nums[last] == less_que.front()) {
less_que.pop_front();
}
last ++;
}
}
return ans;
}
};
长度最小的子数组
https://leetcode.cn/problems/minimum-size-subarray-sum/?envType=study-plan-v2&id=top-interview-150
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int ans = INT_MAX;
int sum = 0;
int last = 0, front = 0;
for (int front = 0; front < nums.size(); front ++) {
sum += nums[front];//放入窗口中
while (sum >= target) {//收集答案并且尝试不停缩小左窗口, 收集最优ans
ans = min(ans, front - last + 1);
sum -= nums[last];//last出窗口
last ++;
}
}
return ans == INT_MAX ? 0 : ans;
}
};