滑动窗口总结

196 阅读5分钟

滑动窗口总结

  • 抓住题目条件,构造滑动窗口
  • 使用左left右right(两个不必每次都同时存在,有的题只需要一个指针)指针
  • right右移,遇到不符合题目条件的需要保留计算结果,并移动left指针,保证窗口(left与right之间的)内的元素符合题目条件

滑动窗口代码模板

// s-> 待遍历字符串 p-> 目标字符串
var slip = function(s, p) {
    let pLen = p.length, sLen = s.length;
    if(pLen > sLen)
        return []
    let result = []
    // 定义左右指针
    let left = 0, right = 0
    // 窗口以及需要满足的条件
    let window = {}, need = {}
    let valid = 0
    for(i of p){
        if(need[i] === undefined)
            need[i] = 0
        need[i]++
        window[i] = 0
    }
    let needSize = Object.keys(need).length
    while(right < sLen){
        const c = s[right]
        right++
        // right右移将字母推入窗口
        if(need[c] !== undefined){
            window[c]++
            ...
        }
        // 满足最终条件推入结果数组
        if(valid === needSize){
            result.push(left)
        }
        // 不满足条件右移left指针直到满足条件
        while(right - left >= pLen){
            const d = s[left]
            left++
            if(window[d] !== undefined){
                ...
            }
        }
    }
    return result
};

举例

无重复字符的最长子串

  • 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 解题思路:
  • 使用map保存进入窗口元素下标
  • right指针右移,当它不在map中时,代表不与之前元素重复,right继续右移
  • 当它在map中存在时,代表该元素之前出现过,此时计算righ-left+1即当前无重复的子串长度,与之前的max做对比,取最大的,并且left指针右移,直到窗口中没有重复元素。
var lengthOfLongestSubstring = function(s) {
    let len = s.length;
    let left = 0, max = 0;
    let map = new Map();
    for(let right = 0; right < len; right++) {
        if(map.has(s[right]) && map.get(s[right]) >= left) {
            left = map.get(s[right]) + 1;
        }
        max = Math.max(max, right - left + 1);
        map.set(s[right], right)
    }
    return max;
};

重复的DNA序列

  • DNA序列 由一系列核苷酸组成,缩写为 'A', 'C', 'G' 和 'T'.。
  • 例如,"ACGAATTCCG" 是一个 DNA序列 。在研究 DNA 时,识别 DNA 中的重复序列非常有用。
  • 给定一个表示 DNA序列 的字符串 s ,返回所有在 DNA 分子中出现不止一次的 长度为 10 的序列(子字符串)。你可以按 任意顺序 返回答案。 解题思路:
  • 该题滑动窗口长度已固定为10,需要找出出现次数不止一次的长度为10的序列
  • 使用map存储序列,key为序列,value为出现次数
  • 该题只需要1个指针,滑动窗口长度为10,窗口组成的字符串存在于map中则map.get(str)+1,否则map.set(str, 1)
  • 最后遍历map,统计value大于1的
    let len = s.length;
    let map = new Map();
    let res = [];
    for(let i = 0;i < len;i ++) {
        let str = s.substr(i, 10)
        let l = str.length;
        if(l === 10) {
            if(map.get(str)) {
                map.set(str, map.get(str) + 1)
            } else {
                map.set(str, 1)
            }
        }
    }
    for(let k of map.keys()) {
        if(map.get(k) > 1) {
            res.push(k);
        }
    }
    return res;
};

替换后的最长重复字符

  • 给你一个字符串 s 和一个整数 k 。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k 次。
  • 在执行上述操作后,返回包含相同字母的最长子字符串的长度。 解题思路:
  • 该题的滑动窗口长度包括重复字符出现的最大次数max+k个非重复字符的长度
  • 使用长度为26的数组,保存每个字母重复出现的次数
  • right指针右移,数组中代表s[right]字母的元素个数++
  • 判断max和数组中代表s[right]字母的元素谁大,即保存最大重复元素个数
  • 当right-left+1(即滑动窗口长度) - max(最大重复元素个数) > k时,说明不重复元素个数>k,此时需要将left指针右移保证窗口内的不重复元素<=k,并且将数组中该重复元素数量-1,最后right-left滑动窗口长度即为结果
var characterReplacement = function(s, k) {
    let len = s.length;
    let num = new Array(26).fill(0);
    let right = 0, left = 0, max = 0;
    while(right < len) {
        num[s[right].charCodeAt() - 65]++;
        max = Math.max(max, num[s[right].charCodeAt() - 65]);
        if(right - left + 1 - max > k) {
            num[s[left].charCodeAt() - 65]--;
            left++;
        }
        right++;
    }
    return right - left;
};

找到字符串中所有字母异位词

  • 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
  • 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。 解题思路:
  • 该题滑动窗口长度为p字符串长度
  • 使用need保存p中字母以及字母个数,needSize代表需要的字母,窗口固定为p的长度,valid保存满足的字母个数,遍历时如果need中有此字母。window也有,并且字母的个数一致,则valid++。
  • 当窗口里的字母以及字母个数满足need时,将该子串push到结果数组中
  • 当窗口长度>p的长度时,移动left指针并从窗口中去除左侧字母。
var findAnagrams = function(s, p) {
    let pLen = p.length, sLen = s.length;
    if(pLen > sLen)
        return []
    let result = []
    let left = 0, right = 0
    let window = {}, need = {}
    let valid = 0
    for(i of p){
        if(need[i] === undefined)
            need[i] = 0
        need[i]++
        window[i] = 0
    }
    let needSize = Object.keys(need).length
    while(right < sLen){
        const c = s[right]
        right++
        if(need[c] !== undefined){
            window[c]++
            if(window[c] === need[c])
                valid++
        }
        if(valid === needSize){
            result.push(left)
        }
        while(right - left >= pLen){
            const d = s[left]
            left++
            if(window[d] !== undefined){
                if(window[d] === need[d])
                    valid--
                window[d]--
            }
        }
    }
    return result
};

可获得的最大点数

  • 几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
  • 每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。
  • 你的点数就是你拿到手中的所有卡牌的点数之和。
  • 给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。 解题思路:
  • 该题的窗口有些隐蔽,只能从开头和结尾拿卡牌,求拿的最大点数,其实可以转化为求n-k窗口内卡牌最小点数。
  • 因此只需要right指针,每次记录n-k窗口内的点数的最小值即可。
var maxScore = function(cardPoints, k) {
    const n = cardPoints.length;
    const windowSize = n - k;
    let sum = 0;
    for (let i = 0; i < windowSize; ++i) {
        sum += cardPoints[i];
    }
    let minSum = sum;
    for (let i = windowSize; i < n; ++i) {
        sum += cardPoints[i] - cardPoints[i - windowSize];
        minSum = Math.min(minSum, sum);
    }
    let totalSum = 0;
    for (let i = 0; i < n; i++) {
        totalSum += cardPoints[i];
    }
    return totalSum - minSum;
}

考试的最大困扰度

  • 一位老师正在出一场由 n 道判断题构成的考试,每道题的答案为 true (用 'T' 表示)或者 false (用 'F' 表示)。老师想增加学生对自己做出答案的不确定性,方法是 最大化 有 连续相同 结果的题数。(也就是连续出现 true 或者连续出现 false)。
  • 给你一个字符串 answerKey ,其中 answerKey[i] 是第 i 个问题的正确结果。除此以外,还给你一个整数 k ,表示你能进行以下操作的最多次数:
  • 每次操作中,将问题的正确答案改为 'T' 或者 'F' (也就是将 answerKey[i] 改为 'T' 或者 'F' )。
  • 请你返回在不超过 k 次操作的情况下,最大 连续 'T' 或者 'F' 的数目。 解题思路:
  • 替换后的最长重复字符思路相似,非重复字符个数不超过k个,该题可以拆成改T或者改F。
  • right指针右移,如果与T/F不一致则sum++,当sum>k时,代表窗口内元素不符合条件,则left指针右移,sum也需要做相应清除。
var maxConsecutiveAnswers = function(answerKey, k) {
    return Math.max(maxConsecutiveChar(answerKey, k, 'T'), maxConsecutiveChar(answerKey, k, 'F'));
}
const maxConsecutiveChar = (answerKey, k, ch) => {
    const n = answerKey.length;
    let ans = 0;
    for (let left = 0, right = 0, sum = 0; right < n; right++) {
        sum += answerKey.charAt(right) !== ch ? 1 : 0;
        while (sum > k) {
            sum -= answerKey[left++] !== ch ? 1 : 0;
        }
        ans = Math.max(ans, right - left + 1);
    }
    return ans;
};