滑动窗口
例题一 最小覆盖字串
滑动窗口算法的思路:
1、我们在字符串 S
中使用双指针中的左右指针技巧,初始化 left = right = 0
,把索引左闭右开区间 [left, right)
称为一个「窗口」。
2、我们先不断地增加 right
指针扩大窗口 [left, right)
,直到窗口中的字符串符合要求(包含了 T
中的所有字符)。
3、此时,我们停止增加 right
,转而不断增加 left
指针缩小窗口 [left, right)
,直到窗口中的字符串不再符合要求(不包含 T
中的所有字符了)。同时,每次增加 left
,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right
到达字符串 S
的尽头。
第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
滑动窗口算法的代码框架:
/* 滑动窗口算法框架 */
function slidingWindow(s:string, t:string) {
// need待匹配计数 window窗口内字符串计数
let need={},window={}
for(let i of t) need[i]=need[i]+1 || 1
let [r,l,valid]=[0,0,0]
while (r < s.length) {
// c 是将移入窗口的字符
const c = s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
console.log(`window: [${l}, ${r})`)
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
const d = s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新
...
}
}
}
开始套框架前,只需要思考以下四个问题:
1、当移动 right
扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动 left
缩小窗口?
3、当移动 left
缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window
计数器;如果一个字符将移出窗口的时候,应该减少 window
计数器;当 valid
满足 need
时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
完整代码
function minWindow(s: string, t: string): string {
let res=""
// len 记录最小字串的长度
let [l,r,valid,len]=[0,0,0,Number.MAX_VALUE]
let need={},window={}
for(let i of t) need[i]=need[i]+1 || 1
while(r<s.length){
if(need[s[r]]){
// 将字符移入窗口
window[s[r]]=window[s[r]]+1 || 1
// 窗口内数据更新
if(window[s[r]]===need[s[r]]){
valid++
}
}
// 右移窗口
r++
// 判断左窗口是否需要收缩
while(valid===Object.keys(need).length){
// 更新最小字串
if(r-l<len){
res=s.slice(l,r)
len=r-l
}
// 窗口内数据更新
if(need[s[l]]){
if(window[s[l]]===need[s[l]]){
valid--
}
// 将字符移出窗口
window[s[l]]--
}
// 右移窗口
l++
}
}
return res
};
需要注意的是,当我们发现某个字符在 window
的数量满足了 need
的需要,就要更新 valid
,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。
当 valid == Object.keys(need).length
时,说明 T
中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。
移动 left
收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。
例题二 字符串排列
function checkInclusion(s1: string, s2: string): boolean {
let [l, r, valid] = [0, 0, 0]
let need = {}, window = {}
for (let s of s1) need[s] = need[s] + 1 || 1
while (r < s2.length) {
if (need[s2[r]]) {
window[s2[r]] = window[s2[r]] + 1 || 1
if (window[s2[r]] === need[s2[r]]) {
valid++
}
}
r++
while (valid === Object.keys(need).length) {
if(r-l===s1.length) return true
if (need[s2[l]]) {
if (window[s2[l]] === need[s2[l]]) {
valid--
}
window[s2[l]]--
}
l++
}
}
return false
};
例题三 找到字符串中所有字母异位词
function findAnagrams(s: string, p: string): number[] {
let res:number[] = []
let need={},window={}
for (let i of p) need[i] = need[i] + 1 || 1
let l:number = 0, r:number = 0
let valid:number=0
while (r < s.length) {
if (need[s[r]]) {
window[s[r]] = window[s[r]] + 1 || 1
if (need[s[r]] === window[s[r]]) {
valid++
}
}
r++
while (r - l >= p.length) {
if (valid === Object.keys(need).length) {
res.push(l)
}
if (need[s[l]]) {
if (window[s[l]] === need[s[l]]) {
valid--
}
window[s[l]]--
}
l++
}
}
return res
};
例题四 无重复字符的最长子串
function lengthOfLongestSubstring(s: string): number {
let res: number = 0
let [r, l] = [0, 0]
let window = {}
while (r < s.length) {
window[s[r]] = window[s[r]] + 1 || 1
while (window[s[r]] > 1) {
window[s[l]]--
l++
}
r++
res=Math.max(res,r-l)
}
return res
};
资料来源:labuladong
例题五替换后的最长重复字符
function characterReplacement(s: string, k: number): number {
let [l,r,n,maxLen]=[0,0,s.length,0]
let window=new Array(26).fill(0)
while(r<n){
//统计每个字符出现的次数
window[s[r].charCodeAt(0)-'A'.charCodeAt(0)]++
//出现最多的次数
maxLen=Math.max(maxLen,window[s[r].charCodeAt(0)-'A'.charCodeAt(0)])
//窗口里的字符长度减去出现最多次数若大于K,则收缩窗口
if(r-l+1-maxLen>k){
window[s[l].charCodeAt(0)-'A'.charCodeAt(0)]--
l++
}
r++
}
return r-l
};
```### 例题五 [904. 水果成篮](https://leetcode.cn/problems/fruit-into-baskets/)
```typescript
function totalFruit(fruits: number[]): number {
// 窗口边界
let [left,right] = [0,0]
let ans=0
// 两个篮子
let ln=fruits[left],rn=fruits[right]
while(right<fruits.length){
// 两种类型,扩大窗口,更新数据
if(fruits[right]===ln||fruits[right]===rn){
right++
ans=Math.max(ans,right-left)
}
else{
// 出现第三种,right-1是第二种
// 此时为了摘最多的果实,ln只能为right-1这种类型
// 否则只能从right开始摘(这样摘的不是最多)
left = right-1
ln = fruits[left]
// 向前遍历到连续的ln类型的第一个,然后从left这个位置开始摘
while(left>=1&&fruits[left-1]===ln) left--
// 将第三种类型记录
rn=fruits[right]
// 更新数据
ans=Math.max(ans,right-left)
}
}
return ans
};
例题六 209. 长度最小的子数组
function minSubArrayLen(target: number, nums: number[]): number {
// 初始化窗口[left,right) 左闭右开
let [left,right] = [0,0]
let ans=Infinity
let sum=0
while(right<nums.length){
// 扩大窗口
sum+=nums[right]
right++
// 窗口内达到条件,开始缩小窗口,并更新状态
while(sum>=target){
ans=Math.min(ans,right-left)
sum-=nums[left]
left++
}
}
return ans===Infinity?0:ans
};