数据结构-滑动窗口算法

276 阅读4分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

介绍

滑动窗口算法,与 双指针算法 类似,也是需要两个指针,但是不同点就是,双指针算法的注重点是两个指针所在的位置上的元素的值,而滑动窗口算法则是两个指针之间的序列值所组成的(窗口)。

滑动窗口,依名知意。首先窗口不是固定位置的,而是动态的移动。而且窗口的大小不一定是固定的,而是跟随两个指针的不同位置,窗口的大小来进行动态改变

1.应用场景

滑动窗口算法的应用场景 一般出现在 数组 或者 链表

2.滑动窗口算法分类

滑动窗口算法分类有两种,一种是 固定窗口,另一种是 滑动窗口

  • 固定窗口

    固定窗口 看似是需要两个指针,但是,由于窗口大小的固定,我们可以根据右指针的位置,就很容易的获取到左指针的位置,通过 left = right - k。使用场景:

    • 子数组最大平均数 I 643题

    • 爱生气的书店老板 1052题

  • 滑动窗口 滑动窗口 则需要左右两个指针,两个指针的位置是根据相关条件的不同,中途会动态改变,进而导致窗口的大小也会动态的改变。应用场景:

    • 替换后的最长重复字符 424题

    • 最小覆盖子串 76题

题目

  1. 子数组最大平均数-643

    给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数

  2. 爱生气的书店老板-1052

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

  3. 替换后的最长重复字符-424

    给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度

  4. 最小覆盖子串-76

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

代码

  • 固定窗口

    • 子数组最大平均数 I 643题

          const findMaxAverage = (nums, k) => {
              let sum = 0
              let maxSum = 0
              const numsLength = nums.length
      
              for (let i = 0; i < numsLength; i++) {
                  // 第一段滑动窗口 [0, k-1]
                  if (i < k) {
                      sum += nums[i]
                      if (i === k - 1) maxSum = sum
                  } else {
                      // 当 i = k 时,滑动窗口开始第一次右移,[i - k + 1, i] 这段就是滑动窗口
                      sum = sum - nums[i - k] + nums[i]
      
                      if (sum > maxSum) maxSum = sum
                  }
              }
              return maxSum / k
          }
      
    • 爱生气的书店老板 1052题

          const maxSatisfied = (customers, grumpy, minutes) => {
            // 老板不生气时的所有满意顾客
            let got = 0
            // 老板控制情绪时滑动窗口中增加的满意顾客,也就是滑动窗口中原本不满意的顾客
            let total = 0
            // 在滑动窗口移动过程中 total 的最大值
            let max = 0
      
            for (let i = 0; i < customers.length; i++) {
              if (!grumpy[i]) got += customers[i]
      
              // 判断是否是第一段滑动窗口 [0, minutes -1]
              if (i < minutes) {
                // 判断老板是否处于生气状态
                if (grumpy[i]) total += customers[i]
              } else {
                // 当 i = minutes 时,滑动窗口开始第一次右移
                // 滑动窗口进行右移,[i - minutes + 1, i] 这段就是滑动窗口
      
                // 因为右移,所以判断左侧离开滑动窗口的 i - minutes ,是否是老板处于生气状态
                if (grumpy[i - minutes]) total -= customers[i - minutes]
      
                // 因为右移,所以判断右侧进入滑动窗口的 i ,是否是老板处于生气状态
                if (grumpy[i]) total += customers[i]
              }
      
              if (total > max) max = total
            }
      
            // 原本的满意顾客 + 情绪控制区最多能收揽的不满意顾客
            return got + max
          }
      
  • 滑动窗口

    • 替换后的最长重复字符 424题 *

          const characterReplacement = (s, k) => {
             let n = s.length
             let left = 0
             let right = 0
             let maxNum = 0
             let strMap = new Array(26).fill(0)
             const getIndex = (str) => str.charCodeAt() - 'A'.charCodeAt()
      
             while (right < n) {
               strMap[getIndex(s[right])]++
               maxNum = Math.max(maxNum, strMap[getIndex(s[right])])
      
               // 窗口宽度 > 最长子串
               if (right - left + 1 > maxNum + k) {
                 // 窗口平移
                 strMap[getIndex(s[left])]--
                 left++
               }
               right++
             }
      
             return n - left
           }
      
    • 最小覆盖子串 76题 *

          const minWindow = (s, t) => {
            // 先制定目标 根据t字符串统计出每个字符应该出现的个数
            let targetMap = makeCountMap(t)
      
            let sl = s.length
            let tl = t.length
            let left = 0 // 左边界
            let right = -1 // 右边界
            let countMap = {} // 当前窗口子串中 每个字符出现的次数
            let min = '' // 当前计算出的最小子串
      
            // 循环终止条件是两者有一者超出边界
            while (left <= sl - tl && right <= sl) {
              // 和 targetMap 对比出现次数 确定是否满足条件
              let isValid = true
              Object.keys(targetMap).forEach((key) => {
                let targetCount = targetMap[key]
                let count = countMap[key]
                if (!count || count < targetCount) {
                  isValid = false
                }
              })
      
              if (isValid) {
                // 如果满足 记录当前的子串 并且左边界右移
                let currentValidLength = right - left + 1
                if (currentValidLength < min.length || min === '') {
                  min = s.substring(left, right + 1)
                }
                // 也要把map里对应的项去掉
                countMap[s[left]]--
                left++
              } else {
                // 否则右边界右移
                addCountToMap(countMap, s[right + 1])
                right++
              }
            }
      
            return min
          }
          const addCountToMap = (map, str) => {
            if (!map[str]) {
              map[str] = 1
            } else {
              map[str]++
            }
          }
      
          const makeCountMap = (strs) => {
            let map = {}
            for (let i = 0; i < strs.length; i++) {
              let letter = strs[i]
              addCountToMap(map, letter)
            }
            return map
          }
      
      

总结

滑动窗口算法 作为和 双指针算法 相似的算法,也是数据结构中 基础算法之一。它也是两个指针起到关键作用,但是由于作用位置和区域是不同的,所以两者进行区分。