建议先读文章,本文解决一类最难掌握的双指针技巧:滑动窗口技巧。
难度在于各种细节问题,比如:如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。
技术:
- 用到的数据结构——哈希表(字典)
- 直接使用对象来代替map,1. 获取键的长度 , 2. 不需要get,set方法。
- 字符串遍历
- window[c] = (window[c] || 0) + 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++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
//printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// 在这里更新最小覆盖子串
...
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
return 所需字符串
}
76. 最小覆盖子串 (困难)
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
needs 和 window 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。
进行窗口内数据的一系列更新:window的字符数量增加,统计覆盖的字符数
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
let need = new Map(), window = new Map();
for (let c of t){
need.set(c, (need.get(c) || 0) + 1); //代码功底
}
let left = right = count = start = 0;
let minlen = Number.MAX_SAFE_INTEGER;
while (right < s.length) {
let c = s[right];
right++;
// 当前字符在t字符串中,才更新窗口的统计
if(need.has(c)) {
window.set(c, (window.get(c) || 0) + 1);
// 等于, 才说明某字符完成覆盖
if (window.get(c) == need.get(c)) {
count++;
}
}
// 判断左侧窗口是否要收缩
while (count == need.size) {
let temp = right - left;
if (temp < minlen) {
start = left;
minlen = temp;
}
let c = s[left];
left++;
if(need.has(c)) {
//再减就不够数覆盖某字符
if (window.get(c) == need.get(c)) {
count--;
}
window.set(c, window.get(c) - 1);
}
}
}
return minlen == Number.MAX_SAFE_INTEGER ? "" : s.substr(start, minlen);
};
567. 字符串的排列 (中等)
注意:输入的 s1 是可以包含重复字符的。排列之一:包含s1中所有字符,不能多余且不包含其他字符。即:窗口大小等于s1的长度,窗口大小固定也能运用上面的框架,先移入后移出。
技巧:要覆盖s1字符时,因为窗口限制了大小,不可能有多余的字符,
区别:如果所有字符都覆盖了,则结束。而上一题则是开始缩圈。
/**
* @param {string} s1
* @param {string} s2
* @return {boolean}
*/
var checkInclusion = function(s1, s2) {
let need = {}, window = {};
for (let c of s1) {
need[c] = (need[c] || 0) + 1;
}
let left = right = count = 0;
let maxlen = s1.length, target = Object.keys(need).length;
while (right < s2.length) {
let c = s2[right]
right++
// 处理移入的字符
if(need[c]) {
window[c] = (window[c] || 0) + 1;
if(window[c] == need[c]) count++;
}
// 判断左侧窗口是否要收缩
while (right-left == maxlen) {
// 判断结果
if (count == target) return true;
let c = s2[left]
left++
// 处理移入的字符
if(need[c]) {
if(window[c] == need[c]) count--;
window[c]--;
}
}
}
return false;
};
个人觉得:有了这个框架,优点:更容易记住,不需要管别人怎么写了。缺点:一开始花时间。