前端搞算法不再难,如何套路解题:滑动窗口类

前言

这不是一个给一道题目而后告诉你题解的系列,而是对于一系列题目进行分类,找出他们解题规律并得出大体框架代码的文章。吃透解一系列题目的规律比会解单个题目有用的多,毕竟你总会遇到没刷过的题。算法

正文

你们对于滑动窗口应该不陌生,在 TCP 协议中就有这个概念的出现,用于控制网络流量,避免拥塞发生。数组

在算法中这个思想也是相似的,多用于解决在一段连续的区间中寻找知足条件的问题,好比说给定一个字符串,寻找出无重复字符的最长子串。该思路主要应用于数组及字符串的数据结构中。markdown

示例

截屏2020-11-04下午10.50.11

滑动窗口主要思路是维护一对指针,在必定条件内右移右指针扩大窗口大小直到窗口内的解不知足题意,此时咱们须要根据状况移动左指针,重复移动左右指针的操做并在区间内求解,直到双指针不能再移动。网络

寻找出无重复字符的最长子串 题目为例,根据上述的思路解题就会很方便:数据结构

var lengthOfLongestSubstring = function(s) {
    // 用于存储指针移动过程当中的值
    let map = {}
    // 双指针
    let left = 0
    let right = 0
    // 结果
    let count = 0
    // 指针移动终止条件
    while (right < s.length) {
        const char = s[right]
        // 根据题意咱们须要寻找不重复的最长子串
        // 当 char 出现时咱们须要移动左指针到重复字符的下一位
        if (char in map) {
            left = Math.max(left, map[char] + 1)
        }
        // 求解
        count = Math.max(count, right - left + 1)
        // 移动右指针并存下索引
        map[char] = right++
    }
    return count
};
复制代码

此题为高频题,你们务必掌握框架

截屏2020-11-05下午10.11.08

框架

根据上题咱们能够得出一个滑动窗口解题的大体框架的伪代码,oop

let left = 0
let right = 0
while (right < size) {
    获取当前索引数据
    right++
    数据更新等操做
    while (窗口须要缩小) {
        left++
        数据移除等操做
    }
}
复制代码

框架中须要变化的几点以下:ui

  • 右指针右移后数据的更新
  • 判断窗口什么时候须要缩小
  • 左指针右移后数据的更新
  • 根据题目求最优解

接下来咱们根据这个框架代码来试着解决几道题目。spa

实战

209. 长度最小的子数组

解题思路:指针

  1. 移动右指针并将移动后的值累加存储起来
  2. 当累加值大于 s 时移动左指针缩小窗口,此时须要更新累加值及咱们须要的解
var minSubArrayLen = function(s, nums) {
    // 定义双指针
    let left = 0
    let right = 0
    // 求解须要用到的变量
    let length = Infinity
    let sum = 0
    // 指针移动终止条件
    while (right < nums.length) {
        // 获取当前索引数据
        sum += nums[right]
        // 缩小窗口条件
        while (sum >= s) {
            // 求解
            length = Math.min(length, right - left + 1)
            // 缩小窗口
            sum -= nums[left++]
        }
        // 扩大窗口
        right++
    }
    return length === Infinity ? 0 : length
};
复制代码

这道题目是 Leetcode 的第 209 题,答案能够说除了小部分的微调以外,基本套用了框架代码。后续的题目你们能够继续跟着这个思路解题,快速掌握经过滑动窗口来解题的套路。

出题频率

438. 找到字符串中全部字母异位词

解题思路:

  1. 经过哈希表存储 p 中的字符出现次数
  2. 移动右指针判断当前字符是否还符合条件
  3. 不符合条件时移动左指针缩小窗口,此时须要更新哈希表
  4. 当前字符不存在哈希表时说明双指针能够直接跳到下一位
var findAnagrams = function(s, p) {
    if (!s.length || !p.length || s.length < p.length) return []
    // 求解须要用到的变量
    const map = {}
    const result = []
    // 定义双指针
    let left = 0, right = 0
    // 把字符串 p 中的字符经过 hash 存储起来
    for (let i = 0; i < p.length; i++) {
        const char = p[i]
        if (!(char in map)) {
            map[char] = 0
        }
        map[char] += 1
    }
    // 指针移动终止条件
    while (right < s.length) {
        const char = s[right]
        // map 中存在字符就移动右指针
        if (map[char] > 0) {
            map[char] -= 1
            right++
        // 不然判断左指针所指向的字符是否存在 map 中
        } else if (map[s[left]] >= 0) {
            map[s[left]] += 1
            left++
        // 不存在的话把左右指针所有挪到下一位
        } else {
            left = right += 1
        }
        // 存储正确解
        if (right - left === p.length) {
            result.push(left)
        }
    }
    return result
};
复制代码

出题频率

76. 最小覆盖子串

出题频率

这道题目和以前的 「找到字符串中全部字母异位词」思路很相似:

  1. 经过哈希表存储 t 中的字符出现次数
  2. 移动右指针判断当前字符是否还符合条件
  3. 不符合条件时移动左指针缩小窗口,此时须要更新哈希表
var minWindow = function(s, t) {
    // 定义双指针
    let left = 0, right = 0
    // 求解须要用到的变量
    let length = Infinity
    let map = {}
    // 遇到 t 中存在的字符时更新 match,注意 t 中存在的字符可能在 s 中出现屡次
    // 所以并非每次都须要更新 match
    let match = 0
    // 记录最短子串开始位置,不能用 left
    let start = 0
    // 把字符串 t 中的字符经过 hash 存储起来
    for (let i = 0; i < t.length; i++) {
        const char = t[i]
        if (!(char in map)) {
            map[char] = 0
        }
        map[char] += 1
    }
    // 指针移动终止条件
    while (right < s.length) {
        const char = s[right]
        // 右指针移动时更新数据
        if (char in map) {
            map[char] -= 1
            if (map[char] >= 0) match += 1
        }
        // 缩小窗口条件
        while (match === t.length) {
            // 寻找到更佳解,保存数据
            if (length > right - left + 1) {
                length = right - left + 1
                start = left
            }
            // 移动左指针而且更新数据
            const char = s[left++]
            if (char in map) {
                if (map[char] === 0) match -= 1
                map[char] += 1
            }
        }
        // 移动右指针
        right++
    }
    return length === Infinity ? '' : s.substring(start, start + length)
};
复制代码

总结

通过上面几道题目的练习,你们应该能看出滑动窗口的思路多用于解决数组及字符串中子元素的问题。

相关文章
相关标签/搜索