前端刷完这12道滑动窗口,就可以出山面试了

本文介绍了滑动窗口算法在前端开发中的应用,特别是在TCP控制流速中的作用。通过分析TCP的滑动窗口机制,解释了固定大小和可变大小滑动窗口的概念,并通过实例展示了如何解决相关算法问题。文章提供了12道相关的LeetCode题目,帮助读者加深对滑动窗口的理解,并鼓励实践以提升算法能力。
摘要由CSDN通过智能技术生成

前言

经常会有人问,作为前端,你在实际工作中用到过哪些算法,之前我回答是,树和位运算,而最近在学习网络模块,发现了和前端,起码是和网络相关的一种算法,那就是 滑动窗口

我们知道在 HTTP1.1 发送请求,TCP 会将请求传输到服务端,而对于 TCP 协议,最重要的能力之一就是控制流速;

当发送方需要发送很多请求的时候,这些请求会阻塞在某一个缓存中等待 TCP 发送,这个后面还有源源不断的请求发起,那总不能一下子全堵在缓存上吧,会炸掉的,这个时候这个模型就是滑动窗口了

image.png

发送过程有三个状态:

  • 绿色是发送并连接成功的
  • 浅绿色是发送,但是还没有收到 ACK 响应的,这个时候有可能会挂掉,所以这个时候发送方还得存着这个请求随时准备重发
  • 白色是等待发送的
  • 后面那些就是被阻塞的请求了

这个时候 TCP 能够缓存的请求数就是一个窗口,每当浅绿色转成深绿色,那么窗口就可以像右边滑动,而窗口还保留的状态依然可以复用,这就是滑动窗口 的魅力了

滑动窗口最大特点是,滑动窗口过程中,保留在窗口里的数据状态直接复用,不需要再次构建,节约资源;

那么接下来我们通过做题来熟悉一下滑窗,并看看是否有更多不一样的情况吧;

正文

根据滑窗窗口大小是否固定,分成了两种:固定大小的窗口可变窗口大小;

前言谈及的 TCP 中的滑窗情况,其实是一个固定大小的滑窗,当然也可以先给定部分大小,然后根据流速进行扩展,那是后续的操作了;

而更多的情况是不固定大小的滑窗,这类滑窗一般都是创建过程中,一股脑子将资源耗尽去扩大窗口,达到一个阈值,然后再收缩窗口,根据具体题目,达到一个平衡了;

这其实就好像是一个快速试错过程,先将情况推到极致了,然后加入对应的变量来收缩窗口,找到比较合适的一个情况,等到合规的情况在窗口里打破了,就重新扩展;

滑窗其实在理解题意的时候,又有点一分为二的感觉,就是我可以将窗口里的状态和窗口外的状态切分开,但是他们又是此消彼长的关系,这样不断权衡,达到一个动态平衡的状态,就是某些题的结果

模板

固定大小的窗口

  • l 初始化为 0
  • 初始化 r, 使得 r-l+1 就是窗口大小
  • 同时移动 l 和 r
  • 判断窗口内的连续元素是否满足题目限定的条件

可变窗口大小

  • l r 都初始化为 0
  • r 指针移动一步
  • 判断窗口内的连续元素是否满足条件
    • 满足,再判断是否需要更新最优解;如果需要则更新,并尝试通过移动 l 指针缩小窗口的大小
    • 不满足,则继续

双滑窗现象

  • 普通的不定滑窗都是先走 r 指针,然后到达触发条件,然后收缩 l 指针,收缩到不达标之后停止,然后 r 指针重新启动
  • 但是有那么一些题目,当 r 指针达标后, l 指针在一段范围内 [l1,l2),且可能与后续的 [r1,r2) 任何两个指针构成的滑窗都会构成合规的滑窗
  • 那么这个时候用单个指针 l 收缩到不符合要求的 l2,那么就只产生 [l1,l2)与 r1 的条件,而本来应该合规的 lx-rx 都被干掉了(lx 在 [l1,l2] 中),因为这个时候 l 已经跑到 l2 处了
  • 这个时候就需要开两个指针 l1, l2 ,每次固定 r 指针的时候,我们找出第一个符合要求的 l1, 和截止位置 l2,然后继续让 r 走,移动过程始终保持两个滑窗 [l1.r],[l2,r],可以保证在整个移动过程所有的情况都考虑到了
  • 这类题目都是求数量,比方说某种情况的子数组有多少个,这样就得将所有情况都弄出来,但是如果只是要求一个极值,比方说这些符合要求的情况中,最小是多少,那么就没必要用双滑窗了,因为 r 指针的移动肯定会扩大窗口,所以 l 指针只需要保留对应的极值(第一个或者最后一个),然后求出极值即可

最后

滑窗是双指针的一种特殊情况,我们在使用双指针处理问题的时候,可能不会考虑前一个窗口里的状态值,只是将所有情况都考虑进行,这样就会有很多计算是重复的,滑窗就是一种优化了的双指针情况。

所以算法还是有点用的,起码在初级的时候,我们可以更好的理解我们使用的工具的内核,而不仅仅只是雾里看花,知其然不知其所以然;

所以加油!!

题目列表

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

@分析
0. 本题与 209. 长度最小的子数组 思路差不多

  1. 这道题窗口就固定为 p 的长度大小了,所以看着是固定窗口大小的题目 – 但是这里用的却是不定窗口的思路,但是窗口长度成了一个限定值,一旦超出限定的窗口大小,就收缩一次
  2. 虽然题目说的是找字幕异位词,但是从实际的例子可以看出,只要是符合 p 的字符对应的子串就 ok 了,不管得出的 ss 是否和 p 是一样的排列
  3. 用 pMap 存储 p 的字符状态,sMap 用来存储固定窗口的值,用 sMap 中的 valid 变量类型数量和 pMap 中的比对,用来判断是否符合要求
  4. 时间复杂度 O(n)
var findAnagrams = function (s, p) {
   
  const pMap = new Map();
  const sMap = new Map();
  let ret = []; // 存储合乎要求的首个字符

  for (let pp of p) {
   
    pMap.set(pp, pMap.get(pp) ? pMap.get(pp) + 1 : 1);
  }
  let valid = 0; // 存储合乎 p 的变量
  let l = (r = 0);
  while (r < s.length) {
   
    const rr = s[r];
    sMap.set(rr, sMap.get(rr) ? sMap.get(rr) + 1 : 1);
    if (sMap.get(rr) === pMap.get(rr)) {
   
      // 两个 key 对应的 value 值一致的时候,才会增加 valid
      valid++;
    }
    // 如果加上这个 r 这个字符,长度超出了固定窗口的长度,则需要先收缩 l, 再判定 
    if (r - l === p.length) {
   
      // 从进入到这里逻辑开始,其实就是属于固定窗口两侧的指针一起跑,这里是 l 指针开始跑,之前因为还没初始化完窗口
      const ll = s[l];
      if (pMap.get(ll) === sMap.get(ll)) {
   
        // 如果收缩过程中的这个值属于 valid 的
        valid--;
      }
      sMap.set(ll, sMap.get(ll) - 1);
      l++;
    }
    if (valid === pMap.size) {
   
      // 合乎要求
      ret.push(l);
    }
    r++;
  }

  return ret;
};

参考视频:传送门

3. 无重复字符的最长子串

// 3. 无重复字符的最长子串

分析
1. 这里求的是最长的子串,证明有很多长度不一的子串,那么就是有很多大小不一的窗口,所以属于窗口不固定的滑窗题
2. 初始化 l r ,初始化一个 map 用来存放窗口里的字符的
3. map 是用来做条件判断的,判断窗口扩展过程中是否和已有的窗口字符重复了,如果重复了,那么就要收缩窗口 s[r]=== s[l] 然后 l++, 
4. 然后不管是否整理窗口, r 指针都会继续扩展下去,所以处理完了,需要重新加上 s[r], 并继续走下去
5. 时间复杂度 ${
   O(n)}$ 因为 r 指针遍历一次,走的过程中遇到重复值 ,l 指针移动,最多 l 也就遍历一次,也就是最多直走了 2n
6. 空间复杂度 $(O(k))$ k 是最大的窗口size
var lengthOfLongestSubstring = function(s) {
   
    const map = new Map() // 这个用来存放
    let l = r = 0 
    let max = 0
    while(r<s.length) {
   
        if(map.has(s[r])){
   
            // 说明窗口里的值已经出现重复了,所以需要整理窗口
            max = Math.max(max,map.size) // 存一下大小
            // 开始收缩窗口,找出 s[r] 同值的那个位置
            while(s[l] !== s[r]){
   
                map.delete(s[l])
                l++
            }
            // 找到了 -- 再移除一下
            map.delete(s[l])
            l++
        }
        // 将当前 s[r] 字符存起来
        map.set(s[r],1)
        r++
    }
    // 这个时候 r 走到底了,
    return Math.max(max,map.size)
}

console.log(lengthOfLongestSubstring("aab"))

分析

  1. 根据第一次分析,发现 map 还有移除操作,觉得不太合适,而且存储的时候也只是存了字符,没有将对应的下标用起来,所以想了下面的改良版
  2. 每一次扩大窗口的时候,判断一下 map 是否存在这个 key,同时判断一下对应的 value 值是否大于等于 l 指针 – 这是为了判断是否存在重复字符在当前窗口里,因为现在已经不删除 map 里的值了,所以要用 value 和 l 的大小进行比较
  3. 如果是窗口里的重复值,那么先存一下当前窗口的最大值,然后将 l 指针跳到重复值的下一个位置,然后更新 s[r] 的位置,继续遍历
  4. 如果不是重复值,就正常存储 s[r] 的位置
  5. 注意,这里不能用 map.size 来判断窗口大小,因为现在 map 存的是所有遍历的字符的集合,所以要用 r-l;因为每次 r 都指向窗口的下一个值,所以直接 r-l, 而不需要 &#
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值