滑动窗口常用技巧总结

概述

在解决 字串问题时,滑动窗口技巧可能经常会使用,其本身思想并不难理解,难在灵活。因而本文从一个 最小覆盖字串问题入手总结一个通用的算法框架以解决常见的滑动窗口问题。

算法与框架

下边我们先看一个最小覆盖子串问题:

滑动窗口常用技巧总结

题目本身不难理解,主要就是从S(source)中找到包含T(target)中全部字幕的一个子串,顺序无所谓,个数相同且 子串中一定是所有可能子串中最短的。

最简单的思路是通过 暴力法,通过两层搜索来解决,但时间复杂度很高,甚至大于O(n^2)。

此类问题实际上我们可以通过 滑动窗口的思路来解决。具体思路如下:

  1. 在字符串S中使用双指针中左右指针的技巧,初始化 left = right = 0,把索引区间 [left,right]称之为一个[窗口]。
  2. 不断的增加right指针扩大窗口 [left,right ],直到窗口中的字符串符合要求(窗口包含T中所有字符)。
  3. 停止增加right,转而 增加left指针,进而缩小窗口直到窗口不再符合要求。同时每增加一个left都要更新一轮结果。
  4. 重复2和3,直到right达到字符串S的尽头。

整个过程思路并不难,其中 第2步相当于在找一个可行解,第3步在优化这个可行解,每轮都进行结果更新,最后找到最优解。

下边我们结合整下边的图来理解算法的整个过程。needs和windows相当于计数器,分别记录T中字符串出现的次数和窗口中的对应字符出现的次数。

第1步:初始状态,left和righ都为0

滑动窗口常用技巧总结

第2步:向右移动right寻找可行解

滑动窗口常用技巧总结

第3步:向右移动left,优化可行解

滑动窗口常用技巧总结

第4步:重复2和3直到,right到达右边界

滑动窗口常用技巧总结

上述过程可以简单写出如下的代码框架:

public String slidingWindow(String s, String t) {
    //定义两个窗口
    Map need = new HashMap<>(), window = new HashMap<>();
    // 初始化need窗口
    for (char c : t.toCharArray()) {
      need.put(c, need.getOrDefault(c, 0) + 1);
    }

    int left = 0, right = 0;
    // 已经和need匹配的字符串个数
    int valid = 0;
    while (right < s.length()) {
      char c = s.charAt(right);
      // move to right
      right++;
      // 进行窗口内一系列数据的更新
      ...

    // 判断左侧窗口是否要收缩
    while (window needs shrink) {
        // d 是将移出窗口的字符
        char d = s.charAt(left);
        // 左移窗口
        left++;
        // 进行窗口内数据的一系列更新
        ...
    }
}

其中两处 ...表示更新窗口数据的地方,根据不同的问题,进行填充即可。

针对最小覆盖子串问题,开始套模板,只需要考虑如下四个问题:

  1. 移动right扩大窗口,即加入字符时需要考虑哪些数据?
  2. 什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?
  3. 当移动left缩小窗口,即移除字符时,应该更新哪些数据?
  4. 我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

针对该问题我们将代码进行填充后得到如何解法:

string minWindow(string s, string t) {
    unordered_map need, window;
    for (char c : t) need[c]++;

    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及长度
    int start = 0, len = INT_MAX;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.count(c)) {
            window[c]++;
            if (window[c] == need[c])
                valid++;
        }

        // 判断左侧窗口是否要收缩
        //必须使用equlals来判断,不能使用 ==
        while (valid.equals(need.size())) {
            // 在这里更新最小覆盖子串
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.count(d)) {
                if (window[d].euqals(need[d])
                    valid--;
                window[d]--;
            }
        }
    }
    // 返回最小覆盖子串
    return len == INT_MAX ?
        "" : s.substr(start, len);
}

应用

接下来我们再看一下另一个中等难度的题目 字符串的排列

滑动窗口常用技巧总结

题意很好理解,就是判断s2是否包含s1的某种排列。我们比较容易想到用暴力法。但会发现时间复杂度过高无法通过。然后考虑到是子串问题,尝试使用滑动窗口方法。

结合模板,考虑两个问题:

  1. 右侧窗口滑动时,做哪些操作
  2. 左侧窗口滑动的条件,以及所做操作

针对第一个问题,我们考虑到当右侧窗口滑动获取一个字符时要判断当前字符是否在need中,如果存在进行windows计数

针对第二个问题,如果窗口的长度大于 &#x5B57;&#x7B26;&#x4E32;t的长度,则需要进行窗口左移操作,进行窗口”瘦身”

该问题具体代码实现如下:

public boolean checkInclusion(String t, String s) {
    if (t.length() > s.length()) {
      return false;
    }
    Map need = new HashMap<>();
    Map window = new HashMap<>();
    // init need
    for (char c : t.toCharArray()) {
      need.put(c, need.getOrDefault(c, 0) + 1);
    }
    // define variable
    int left = 0, right = 0;
    int valid = 0;
    while (right < s.length()) {
      char c = s.charAt(right);
      right++;
      // update right window
      if (need.containsKey(c)) {
        window.put(c, window.getOrDefault(c, 0) + 1);
        if (need.get(c).equals(window.get(c))) {
          valid++;
        }
      }
      // shrink left window
      // 每一次窗口的尺寸比need的尺寸大的时候都会进行瘦身操作,一直移动到比need的尺寸小1结束
      while (right - left >= t.length()) {
        if (valid == need.size()) {
          return true;
        }
        char d = s.charAt(left);
        left++;
        if (need.containsKey(d)) {
          if (window.get(d).equals(need.get(d))) {
            valid--;
          }
          window.put(d, window.get(d) - 1);
        }
      }
    }
    return false;
  }

总结

简单来说滑动窗口问题其实只要记下这个框架,大部分类似问题都可迎刃而解。

参考

  1. https://labuladong.gitbook.io/algo/shu-ju-jie-gou-xi-lie/2.5-shou-ba-shou-shua-shu-zu-ti-mu/hua-dong-chuang-kou-ji-qiao-jin-jie

Original: https://www.cnblogs.com/goWithHappy/p/slide-window.html
Author: vcjmhg
Title: 滑动窗口常用技巧总结

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/599443/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Python–module–OS

    csharp;gutter:true; import os a = os.getcwd() # 获取当前的操作目录 b = os.chdir("C:\Users&quot…

    数据库 2023年6月9日
    090
  • Mysql性能调优-工具篇

    首先祭出官方文档(这是5.7的,请自行选择版本): 如果你不想读英语,只需阅读这篇文章: [En] If you don’t want to read English,…

    数据库 2023年5月24日
    082
  • PHP最全编码规约

    1.1 标签 (1)【强制】PHP 程序可以使用或来界定 PHP 代码,在 HTML 页面中嵌入纯变量时,可以使用这样的形式,不可使用其他的标签变种。 正例: (2)【强制】纯 P…

    数据库 2023年6月14日
    0113
  • MySQL45讲之随机查询和临时表

    本文介绍 MySQL 随机查询的工作流程、优化随机查询的方式、和临时表。 工作流程 根据下表结构建立 words 表,并通过过程插入 10000 条模拟数据。 CREATE TAB…

    数据库 2023年5月24日
    084
  • 基于Vue简易封装的快速构建Echarts组件 — fx67llQuickEcharts

    fx67llQuickEcharts A tool to help you use Echarts quickly! npm 组件说明 这本来是一个测试如何发布Vue组件至npm库…

    数据库 2023年6月11日
    099
  • MySQL快速创建800w条测试数据表&深度分页

    MySQL快速创建800w条测试数据表&深度分页 如果在普通表格中插入条,效率太低,但内存表的插入速度很快,可以先创建内存表,插入数据,然后再导入到普通表格中。 [En] …

    数据库 2023年5月24日
    0121
  • docker 单机部署redis集群

    docker 部署redis集群 1、创建redis网卡 docker network create redis –subnet 172.38.0.0/16 查看网卡信息 doc…

    数据库 2023年6月11日
    087
  • RoundRobin

    RoundRobin LoadBalanceRound-Robin既是轮询算法,是按照公约后的权重设置轮询比率,即权重轮询算法(Weighted Round-Robin) ,它是基…

    数据库 2023年6月11日
    090
  • MySQL实战45讲 14

    14 | count(*)这么慢,我该怎么办? 在开发系统的时候,你可能经常需要 计算一个表的行数,比如一个交易系统的所有变更记录总数。 随着系统中记录数越来越多,select c…

    数据库 2023年6月16日
    0102
  • mysql开启二进制日志

    打开xhell进入系统 进入mysql配置文件目录 执行 cd /etc/mysql 首先找到my.cnf这个配置文件,然后使用vim进入文件编辑 放开我标记的地方。 注意我标记的…

    数据库 2023年6月6日
    0119
  • 第二十章 AOP开发中的坑

    问题 //在同一个业务类中,一个业务方法调用另一个业务方法 //问题: login方法添加有额外功能 // register方法没有添加额外功能 public class User…

    数据库 2023年6月14日
    071
  • MySQL新建用户与授权

    test为用户名,1234为密码,%意思是任何一台主机都可以登录,如果只能本机登录则设置为localhost 说明:test为数据库的名称 说明:第一个test为数据库的名称,第二…

    数据库 2023年6月9日
    082
  • Centos7.9编译OpenSSH的rpm安装包并升级OpenSSH

    本文介绍如何通过openssh-9.0p1.tar.gz制作openssh的rpm安装包,并升级openssh到9.0。 下载openssh-9.0p1.tar.gz 编译成rpm…

    数据库 2023年6月14日
    094
  • Javaweb07-三层架构(BaseDao)

    1、BaseDao 持久层业务接口实现类的公共父类,定义了jdbc操作数据库的所有公共方法,方便子类继承; import java.io.InputStream; import j…

    数据库 2023年6月16日
    0102
  • 详解 Serverless 架构的 6 大应用场景

    Serverless 架构将成为未来云计算领域重要的技术架构,将会被更多的业务所采纳。进一步深究,Serverless 架构在什么场景下有优秀的表现,在什么场景下可能表现得并不是很…

    数据库 2023年6月14日
    082
  • day04-MySQL常用函数

    5.MySQL常用函数 5.1合计/统计函数 5.1.1合计函数-count count 返回行的总数 Select count(*)|count (列名) from table_…

    数据库 2023年6月11日
    089
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球