算法刷题系列——滑动窗口

697 阅读2分钟

滑动窗口

算法主要思想

用左右指针维护一个窗口(连续的子数组/子串),根据题目在遍历数组或者字符串的时候动态调整两个指针(一般都是++),遇到可行解就进行记录。

let len = s.length;//范围
let  left = 0, right = 0;
while(right < len){
	...未达到要求时
	right++;//扩张
    while(条件){
    	...达到要求时进行一个可行解的计算
    	left++;//收缩
    }
}

leetcode题目

209 长度最小的子数组


给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/mi… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。



解题思路:

求最短的和>=s的子数组,子数组一定是连续的,所以可以用一个窗口表示这个连续的子数组,[left...right]代表这个无重复的数组。 在遍历数组的时候,如果和<s就right++扩张右边界,如果和>=s就记录长度,然后left++收缩左边界,最后求这个窗口的最小长度即可。


/**
 * @param {number} s
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function(s, nums) {
    let min = Infinity;//定义为最大值
    let right = left = 0;
    let len = nums.length;
    if(len===0)//如果数组为空就直接返回0
        return 0;
    let sum = nums[0];
    while(right < nums.length){
        if(sum >= s){//如果和>=s
            min = Math.min(right - left + 1,min);
            sum -= nums[left];
            left++;
        }else{//如果不符合s
            if(++right === len)//以防溢出
                break;
            sum += nums[right];
        }
    }
    min = (min===Infinity)?0:min;//如果没有合适的解就返回0
    return min;
};

3 无重复字符的最长子串


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

输入: s = "abcabcbb"
输出: 3 
输入: s = "bbbbb"
输出: 1
输入: s = "pwwkew"
输出: 3
输入: s = ""
输出: 0

提示:

0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。



解题思路:

求最长的无重复字符的子串,子串一定是连续的,所以可以用一个窗口表示这个连续的子串,[left...right]代表这个无重复的子串。在遍历字符串的时候,如果遇到不重复的字符就right++扩张右边界,如果遇到相同的字符就left++收缩左边界到那个被重复的字符之后。求这个窗口的最大值即可。


/**
 * @param {string} s
 * @return {number}
 */
let lengthOfLongestSubstring = function(s) {
    let maxV = 0;
    let map = {};//用于存储对应字符出现的频率
    let left = 0;
    let right = -1;//右边界初始值设为-1
    while(left < s.length){
        let next = s[right+1];
        if(next!=undefined && !map[next]){//遇到不重复的字符右边界扩张
            map[next] = 1;
            right++;//不重复子串是[left...right]
        }else{//遇到重复的字符 左边界收缩到相同字符 (每次收缩一个字符,但由于没有到达相同的字
              //符所以一直会走else分支)
            map[s[left]] = 0;
            left++;
        }
        maxV = Math.max(right - left + 1,maxV);
    }
    return maxV;
};

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


给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明: 字母异位词指字母相同,但排列不同的字符串。 不考虑答案输出的顺序。

输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。 

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/fi… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。



解题思路1:

用两个指针确定左右边界,然后判断窗口中的字符串是否和目标字符串匹配,若果匹配,左边界存储在数组中,最后进行输出。 复杂度主要在于,每次都要记录窗口中的字母出现的频率,然后和目标字符串进行比较。


/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
let  findAnagrams = function(s, p) {
    let len = p.length;
    let map = {};
    for(let i = 0; i < len; i++){
        if(!map[p[i]]){
            map[p[i]] = 1;
        }else{
            map[p[i]]++;
        }
    }
    let res = [];
    let left = 0;
    let right = len - 1;
    while(right < s.length){
        if(isSame(s,map,left,right)){
            res.push(left);
        }
        left++;
        right++;
    }
    return res;
};
//判断s[left,right]和p是否是字母异位词 
let isSame  = function(s,map,left,right){
    
    let smap = {};//存储窗口中的值
    for(let i = left; i <= right; i++){
        if(!smap[s[i]]){
            smap[s[i]] = 1;
        }else{
            smap[s[i]]++;
        }
    }

    let flag = true;
    let keys = Object.keys(map);//获取目标map中所有键
    for(let key of keys){
        if(!smap[key] || smap[key] != map[key]){
            flag = false;
            break;
        }
    }

    return flag;
}

解题思路2:

用滑动窗口的模板


/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
let  findAnagrams = function(s, p) {
    let targetMap = {};
    let winMap = {};
    for(let i = 0; i < p.length; i++){
        if(!targetMap[p[i]]){
            targetMap[p[i]] = 1;
            winMap[p[i]] = 0;
        }else{
            targetMap[p[i]]++;
        }
    }
    let left = 0,right = 0;
    let match = 0;
    let keyLength = Object.keys(targetMap).length;
    let res = [];
    while(right < s.length){
        if(targetMap[s[right]]){//如果当前字母在目标字符串里
            winMap[s[right]]++;//频率+1
            if(winMap[s[right]]===targetMap[s[right]]){//频率相等
                match++;
            }
        }
        right++;
        while(match === keyLength){
            if(right - left === p.length){
                res.push(left);
            }
            if(targetMap[s[left]]){
                winMap[s[left]]--;
                if(winMap[s[left]] < targetMap[s[left]])
                    match--;
            }
            left++;
        }
    }
    return res;
}

76 最小覆盖子串


给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
输入:s = "a", t = "a"
输出:"a"

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/mi… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


/**
 * @param {string} s
 * @param {string} t
 * @return {string}
**/
var minWindow = function(s, t) {
    let left = 0;
    let right  = 0;
    let targetMap = {};
    let winMap = {};
    //记录目标字符串的字符频率
    for(let i = 0; i < t.length; i++){
        if(!targetMap[t[i]]){
            targetMap[t[i]] = 1;
            winMap[t[i]] = 0;
        }else{
            targetMap[t[i]]++;
        }
    }

    let minLength = s.length;
    let flag = false;//表示是否存在覆盖所有子串的串
    let match = 0;
    let minl = 0, minr = 0;
    let keyLength = Object.keys(targetMap).length;//表示一共要匹配多少种字母
    while(right < s.length){
        if(targetMap[s[right]]){//有相同的字母时
            winMap[s[right]]++;
            if(winMap[s[right]] === targetMap[s[right]]){
                match++;
            }
        }
        right++;//向右扩张
        while(match === keyLength){//出现了可行解
            flag = true;
            if(minLength >= right - left){//判断是否是比之前的更优解
                minl = left;
                minr = right;
                minLength = right - left;
            }
            //向右收缩
            if(targetMap[s[left]]){//如果碰到了相应的字母 收缩后就可能不是可行解了
                winMap[s[left]]--;
                if(winMap[s[left]] < targetMap[s[left]]){
                    match--;
                }
                    
            }
            left++;
        }
    }
    
    return flag? s.slice(minl,minr) : "";

};

239 滑动窗口的最大值


给你一个整数数组 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) 链接:leetcode-cn.com/problems/sl… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。



解题思路1:

暴力解法,用两个指针确定左右边界,然后对其中的子数组使用Math.max()求出最大值,存储在数组中,最后进行输出。时间复杂度是O(nk)


/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
let maxSlidingWindow = function(nums, k) {
    let res = [];
    if(nums.length < k){
        res.push(max(nums,0,nums.length));
        return res;
    }
    for(let left = 0, right = left + k; right <= nums.length; left++,right++){
        res.push(max(nums,left,right));
    }
    return res;

};

let max = function(nums,l,r){
    let max = -Infinity;
    for(let i = l; i < r; i++){
        //max = (max > nums[i]) ? max : nums[i]; //这种写法会超时
         max = Math.max(max,nums[i]);
    }
    return max;
}

TODO:解题思路2:

单调队列解法