js滑动窗口的应用

1,621 阅读2分钟

滑动窗口主要用来解决寻找满足一定条件的连续区间的性质-如长度问题。由于区间连续,因此当区间变化时,可以利用旧有的计算结果进行剪枝,从而减少计算量。例如“满足xx的最x的子数组”问题就可以用滑动窗口解决。

滑动窗口有两种类型,分别是固定大小非固定大小

非固定大小类型的问题,以解决最长无重复子树组举例:

image.png

非固定大小滑动窗口的基本原理:

  1. 初始化left = 0, right = 0
  2. 向右移动right
  3. 判断窗口内的连续元素是否满足条件
    • 如果满足,再判断是否需要更新最优解,如果需要则更新
    • 如果不满足,则向右移动left到最合适的位置

image.png

知道原理来,实现起来也并不复杂:

function maxLength( arr ) {
    let maxLen = 0;
    let map = new Map();
    // 用双指针模拟一个滑动窗口,窗口向右滑动
    for(let left = 0, right = 0; right < arr.length; ++right){
        // 遇到重复数字
        if(map.has(arr[right])){
            // 因为有可能遇到的重复数字的位置在left前面,比较当前left的位置与重复数字的位置
            // 如当输入是abcba,因为b重复了,所以left已经移动到了c
            // 当right移动到a时,虽然a重复了,但是left已经移动到a后面了
            left = Math.max(left, map.get(arr[right]) + 1);          
        }
        map.set(arr[right], right);
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

再来一道leetcode的题目1208. 尽可能使字符串相等

image.png

var equalSubstring = function(s, t, maxCost) {
    let cost = 0;
    let maxLen = 0;
    for(let left = 0, right = 0; right < s.length; ++right){
        // 计算当前字符进行转换需要的开销   
        const rightCost = Math.abs(s.charCodeAt(right) - t.charCodeAt(right));
        // 如果开销总和大于最大预算,则将左指针往右移,并且从开销中减去左指针对应的开销
        // 由于右指针指向的字符转换开销可能比较大,左指针需要向右移动多个位置
        while(cost + rightCost > maxCost){
            cost -= Math.abs(s.charCodeAt(left) - t.charCodeAt(left));
            ++left;
        }
        cost += rightCost;
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
};

固定大小的滑动窗口问题,以1456.定长子串元音中的最大数目为例:

image.png

固定窗口就不需要用到双指针了,只需要right指针,left指针可以轻易地通过right - k计算出来

思路如下:

  1. 初始化right指针, 并构造大小为k的滑动窗口
  2. right指针指向位置k,窗口开始滑动
  3. 将最左边的元素移出窗口,可通过right - k计算其位置,并做相应的处理
  4. 将right指针指向的元素添加至窗口,判断是否需要更新最优解,如果需要则更新,否则继续
/**
 * @param {string} s
 * @param {number} k
 * @return {number}
 */
 var maxVowels = function(s, k) {
    let curCount = 0;
    let maxCount = 0;
    if(s.length < k || k === 0) return maxCount;
    // 构造大小为k的窗口
    for(let right=0; right<k; right++){
        if(isVowel(s[right])){
            ++curCount;
            maxCount = Math.max(maxCount, curCount);
        }
    }
    // 窗口向右移动
    for(let right=k; right<s.length; right++){
        // 将左边界元素移除
        if(isVowel(s[right-k])) --curCount;
        // 将右边界元素移入
        if(isVowel(s[right])){
            ++curCount;
            // 判断是否需要更新最优解
            maxCount = Math.max(maxCount, curCount);
        }
    }
    // 返回最优解
    return maxCount;
};
// 判断是否是元音字母
var isVowel = (ch) => {
    if(['a', 'e', 'i', 'o', 'u'].includes(ch)) return true;  
    return false;
}

使用滑动窗口解决的问题通常是暴力解法的优化,掌握这一类问题最好的办法就是练习。leetcode上已经有滑动窗口问题系列了,滑动窗口问题系列