滑动窗口详解

379 阅读4分钟

定义

滑动窗口是双指针的高级用法,通常用來将嵌套的循环问题,转换为单循环问题來降低时间复杂度。

类型分为以下

  • 固定窗口大小
  • 窗口大小不固定,求解最大的满足条件的窗口
  • 窗口大小不固定,求解最小的满足条件的窗口

伪代码框架如下:

int left = 0, right = 0;
while (right < s.size()) {
    window.add(s[right]);
    right++;
    
    while (valid) {
        window.remove(s[left]);
        left++;
    }
}

滑动窗口常见题

滑动窗口最大值

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值 

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

来源:leetcode-cn.com/problems/sl…

var maxSlidingWindow = function(nums, k) {
    let n = nums.length;
    class slideWindow {
        constructor() {
            this.data = []
        }
        push(val) {
            while(this.data.length > 0 && this.data[this.data.length - 1] < val) {
                this.data.pop()
            }
            this.data.push(val)
        }
        pop(val) {
            if(this.data.length > 0 && this.data[0] === val) {
                this.data.shift()
            }
        }
        max() {
            return this.data[0]
        }
    }
    let res = [];
    let window = new slideWindow();
    for (let i = 0; i < n; i++) {
        if(i < k - 1) {
            window.push(nums[i])
        } else {
            window.push(nums[i]);
            res.push(window.max());
            window.pop(nums[i - k + 1])
        }
    }
    return res;
};

无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

來源:leetcode-cn.com/problems/lo…

var lengthOfLongestSubstring = function(s) {
    let window = {}, l = 0, r = 0, count = 0;
    while(r < s.length) {
        let c = s[r];
        r++;
        window[c] ? window[c]++ : window[c] = 1;
        while(window[c] > 1) {
            let d = s[l];
            l++;
            window[d]--;
        }
        count = Math.max(count, r - l);
    }
    return count;
};

最小覆盖子串

给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。

输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"

来源:leetcode-cn.com/problems/mi…

var minWindow = function(s, t) {
    let window = {}, need = {}, l = 0, r = 0, count = 0, start = -1, minLen = Infinity;
    for (let j = 0; j < t.length; j++) {
        let tmp = t[j];
        need[tmp] ? need[tmp]++ : need[tmp] = 1;
    }
    let tkeylen = Object.keys(need).length;
    while(r < s.length) {
        let c = s[r];
        r++;
        if(need[c]) {
            window[c] ? window[c]++ : window[c] = 1;
            if(window[c] === need[c]) {
                count++
            }
        }
        while(count === tkeylen) {
            if(r - l < minLen) {
                start = l;
                minLen = r - l;
            }
            let d = s[l];
            l++;
            if(need[d]){
                if(window[d] === need[d]) {
                    count--;
                }
                window[d]--
            }
        }
    }
    return minLen === Infinity ? "" : s.substr(start, minLen);
};

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

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

输入:
s: "cbaebabacd" p: "abc"

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。

来源:leetcode-cn.com/problems/fi…

var findAnagrams = function(s, p) {
    let window = {}, need = {}, l = 0, r = 0, count = 0, res = [];
    for (let j = 0; j < p.length; j++) {
        let tmp = p[j];
        need[tmp] ? need[tmp]++ : need[tmp] = 1;
    }
    let tkeylen = Object.keys(need).length;
    while(r < s.length) {
        let c = s[r];
        r++;
        if(need[c]) {
            window[c] ? window[c]++ : window[c] = 1;
            if(window[c] === need[c]) {
                count++
            }
        }
        while(r - l >= p.length){
            if(count === tkeylen) {
                res.push(l)
            }
            let d = s[l];
            l++;
            if(need[d]) {
                if(window[d] === need[d]) {
                    count--
                }
                window[d]--
            }
        }
    }
    return res;
};

水果成篮

在一排树中,第 i 棵树产生 tree[i] 型的水果。你可以从你选择的任何树开始,然后重复执行以下步骤:

  1. 把这棵树上的水果放进你的篮子里。如果你做不到,就停下来
  2. 移动到当前树右侧的下一棵树。如果右边没有树,就停下来。

请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。
你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。用这个程序你能收集的水果树的最大总量是多少?

输入:[0,1,2,2]
输出:3
解释:我们可以收集 [1,2,2]
如果我们从第一棵树开始,我们将只能收集到 [0, 1]

來源:leetcode-cn.com/problems/fr…

var totalFruit = function(tree) {
    let l = 0, r = 0 , count = 0, window = {}, res = 0;
    while(r < tree.length) {
        let c = tree[r];
        if(window[c]) {
            window[c]++;
        } else {
            window[c] = 1;
            count++
        }
        while(count > 2) {
            let d = tree[l];
            l++;
            window[d]--;
            if(!window[d]) {
                count--
            }
        }
        res = Math.max(res, r - l + 1);
        r++;
    }
    return res;
};

和相同的二元子数组

在由若干 01 组成的数组 A 中,有多少个和为 S非空子数组。

输入:A = [1,0,1,0,1], S = 2
输出:4
解释:
如下面黑体所示,有 4 个满足题目要求的子数组:
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]

來源:leetcode-cn.com/problems/bi…

var numSubarraysWithSum = function(A, S) {
    let l = r = 0, sum = 0, count = 0;
    while(r < A.length) {
        let c = A[r];
        sum += c;
        while(sum > S) {
            let d = A[l];
            sum -= d;
            l++;
        }
        if(sum === S) {
            let tmp = sum, j = l;
            while(j <= r && tmp === S) {
                count++;
                tmp -= A[j];
                j++
            }
        }
        r++;
    }
    return count;
};

K 个不同整数的子数组

给定一个正整数数组 A,如果 A 的某个子数组中不同整数的个数恰好为 K,则称 A 的这个连续、不一定独立的子数组为好子数组。

输入:A = [1,2,1,2,3], K = 2
输出:7
解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].

來源:leetcode-cn.com/problems/su…

var subarraysWithKDistinct = function(A, K) {
    let len = A.length, l = 0, r = 0, c = 0, res = 0;
    let need = new Array(len + 1).fill(0)
    for (; r < len; ++r) {
        if(need[A[r]]++ == 0) ++c;
        while( c > K ) {
            if(--need[A[l]] === 0) --c;
            ++l;
        }
        let t = l;
        if(c === K) {
            while(c === K) {
                if(--need[A[t]] === 0) --c;
                ++t;
                ++res;
            }
            for (let j = l; j < t; ++j) {
                if(need[A[j]]++ === 0) ++c;
            }
        }
    }
    return res;
};

 替换子串得到平衡字符串

有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。 假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。 给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。 你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。 请返回待替换子串的最小可能长度。 如果原字符串自身就是一个平衡字符串,则返回 0。  

输入:s = "QQWE"
输出:1
解释:我们需要把一个 'Q' 替换成 'R',这样得到的 "RQWE" (或 "QRWE") 是平衡的。

來源:leetcode-cn.com/problems/re…

var balancedString = function(s) {
    let window = {}, need = {}, l = 0, r = 0, count = 0;
    let times = s.length / 4;
    for (let i of s) {
        need[i] ? need[i]++ : need[i] = 1;
    }
    for (let key of Object.keys(need)) {
        if(need[key] > times) {
            need[key] = need[key] - times;
        } else {
            delete need[key]
        }
    }
    let keylens = Object.keys(need).length;
    if(keylens === 0) return 0;
    let minlen = Infinity;
    while( r < s.length) {
        let c = s[r];
        r++;
        if(need[c]) {
            window[c] ? window[c]++ : window[c] = 1;
            if(window[c] === need[c]) {
                count++
            }
        }
        while(count === keylens) {
            minlen = Math.min(minlen, r - l);
            let d = s[l];
            l++;
            if(need[d]) {
                window[d]--;
                if(window[d] < need[d]) {
                    count--
                }
            }
        }
    }
    return minlen;
};

串联所有单词的子串

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。 注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

输入:
  s = "barfoothefoobarman",
  words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。

來源:leetcode-cn.com/problems/su…

var findSubstring = function(s, words) {
    let left = 0, right = 0, len = words.length;
    if(len === 0) return [];
    let res = [], gaplen = words[0].length;
    let need = {}, window = {}, count = 0;
    words.forEach(item => {
        need[item] ? need[item]++ : need[item] = 1;
    })
    console.log(need)
    let keylen = Object.keys(need).length;
    for (let i = 0; i < gaplen; i++) {
        right = left = i;
        count = 0;
        while(right <= s.length - gaplen) {
            let c = s.substring(right, right + gaplen);
            right += gaplen;
            if(need[c]) {
                window[c] ? window[c]++ : window[c] = 1;
                if(window[c] === need[c]) {
                    count++;
                }
            }
            while(left < right && count === keylen) {
                if(Math.floor((right - left) / gaplen) === len) {
                    res.push(left)
                }
                let d = s.substring(left, left + gaplen);
                left += gaplen;
                window[d]--;
                if(need[d] && window[d] < need[d]) {
                    count--;
                }
            }
        }
        window = {}
    }
    return res;
};

最短超串

假设你有两个数组,一个长一个短,短的元素均不相同。找到长数组中包含短数组所有的元素的最短子数组,其出现顺序无关紧要。 返回最短子数组的左端点和右端点,如有多个满足条件的子数组,返回左端点最小的一个。若不存在,返回空数组。

输入:
big = [7,5,9,0,2,1,3,5,7,9,1,1,5,8,8,9,7]
small = [1,5,9]
输出: [7,10]

來源:leetcode-cn.com/problems/sh…

var shortestSeq = function(big, small) {
    let window = {}, need = {}, l = -1, r = -1, count = 0;
    small.forEach(item => need[item] ? need[item]++ : need[item] = 1)
    let length = big.length + 1;
    let ansl = -1, ansr = -1;
    while(r < big.length) {
       r++;
       let c = big[r];
       if(need[c]) {
           window[c] ? window[c]++ : window[c] = 1;
            if(window[c] === 1) {
                ++count;
            }
       }           
       while(count === small.length && big.length - l > small.length) {
           l++;
           let d = big[l];
           if (need[c]) {
               if(--window[d] == 0) {
                    count--;
                    if(length > r - l + 1) {
                        ansl = l;
                        ansr = r;
                        length = r - l + 1;
                    }
                }
           }          
       }
    }
    return ansl === -1 && ansr === -1 ? [] : [ansl, ansr];
};

字符串的排列

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,第一个字符串的排列之一是第二个字符串的子串。

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").

來源:leetcode-cn.com/problems/pe…

var checkInclusion = function(s1, s2) {
    let window = {}, need = {}, l = 0, r = 0, count = 0;
    for (let j = 0; j < s1.length; j++) {
        let tmp = s1[j];
        need[tmp] ? need[tmp]++ : need[tmp] = 1;
    }
    let tkeylen = Object.keys(need).length;
    while(r < s2.length) {
       let c = s2[r];
       r++;
       if(need[c]){
           window[c] ? window[c]++ : window[c] = 1;
           if(window[c] === need[c]) {
               count++
           }
       }
       while(r - l >= s1.length) {
           if(count == tkeylen) {
               return true
           }
           let d = s2[l];
           l++;
           if(need[d]) {
               if(window[d] === need[d]) {
                   count--;
               }
               window[d]--
           }
       }
    }
    return false;
};

爱生气的书店老板

今天,书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客(customers[i])会进入书店,所有这些顾客都会在那一分钟结束后离开。 在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。 书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X 分钟不生气,但却只能使用一次。 请你返回这一天营业下来,最多有多少客户能够感到满意的数量。  

输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3
输出:16
解释:
书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.

來源:leetcode-cn.com/problems/gr…

var maxSatisfied = function(customers, grumpy, X) {
    if(grumpy.length < 0) return 0;
    let len = grumpy.length, l = 0, r = 0, count = 0, max = 0, res = 0;
    while (r < len) {
        if(grumpy[r] === 1) {
            count += customers[r]
        }       
        while(r - l + 1 > X) {
            if(grumpy[l] === 1) {
                count -= customers[l]
            }
            l++;
        }
        r++;
        max = Math.max(count, max)
    }
    for (let i = 0; i < len; i++) {
        res += (grumpy[i] === 0 ? customers[i] : 0);
    }
    return res + max;
};

尽可能使字符串相等

给你两个长度相同的字符串,s 和 t。 将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。 用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。 如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。 如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。  

输入:s = "abcd", t = "bcdf", cost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3

來源:leetcode-cn.com/problems/ge…

var equalSubstring = function(s, t, maxCost) {
    let l = 0;
    let r = 0;
    let maxlen = 0;
    let tmp = 0;
    while(r < s.length) {
        tmp = Math.abs(s[r].charCodeAt() - t[r].charCodeAt());
        if(maxCost - tmp < 0) {
            l++;
            maxCost += Math.abs(s[l - 1].charCodeAt() - t[l - 1].charCodeAt())
        }
        maxCost -= tmp;
        maxlen = Math.max(maxlen, r - l + 1);
        r++;
    }
    return maxlen;
};

单字符重复子串的最大长度

如果字符串中的所有字符都相同,那么这个字符串是单字符重复的字符串。 给你一个字符串 text,你只能交换其中两个字符一次或者什么都不做,然后得到一些单字符重复的子串。返回其中最长的子串的长度。  

输入:text = "aaabaaa"
输出:6

来源:leetcode-cn.com/problems/sw…

var maxRepOpt1 = function(text) {
    let res = 0, n = text.length, l = 0, r = 0, mf = 0, mc = text[0];
    let count = {}; let ds = {};
    for (let r = 0; r < n; r++) {
        let c = text[r];
        count[c] = count[c] + 1 || 1;
    }
    let keylen = Object.keys(count).length;
    for (let r = 0; r < n; ++r) {
        let c = text[r];
        if(mf < ds[c]) {
            mf = ds[c];
            mc = c;
        }
        if(r - l + 1 - mf > 1) {
            let d = text[l];
            ds[d]--;
            l++
        }
        res = Math.max(res, r - l + 1)
    }
    return Math.min(res, count[mc])
};

和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。  

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

来源:leetcode-cn.com/problems/he…

var findContinuousSequence = function(target) {
    let list = [];
    let left = 1;
    let right = 1;
    let sum = 0;
    while(left < target/2) {
        if(sum < target) {
            sum += right;
            right++;
        } else if (sum > target) {
            sum -= left;
            left++;
        } else {
            let arr = [];
            for (let i = left; i < right; i++) {
                arr.push(i);
            }
            list.push(arr);
            sum -= left;
            left++;
        }
    }
    return list;
};