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

6,596 阅读3分钟

前言

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

正文

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

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

示例

截屏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

框架

根据上题我们可以得出一个滑动窗口解题的大致框架的伪代码,

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

框架中需要变化的几点如下:

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

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

实战

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)
};

总结

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