【算法】基础算法002之滑动窗口(二)

news2024/11/28 2:37:12

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

 5.水果成篮(medium)

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

7.串联所有单词的子串(hard) 

8.最小覆盖字串(hard)


前言

滑动窗口专题续作,本篇文章继续围绕滑动窗口进行讲解,并辅以实战OJ题帮助理解。


 欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。 

=========================================================================

GITEE相关代码:🌟樊飞 (fanfei_c) - Gitee.com🌟

=========================================================================


 5.水果成篮(medium)

904. 水果成篮 - 力扣(LeetCode)https://leetcode.cn/problems/fruit-into-baskets/description/

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

​ 阅读题目,其实就是找出一个最长的子数组的长度,要求子数组中不能超过两种元素。

思路:

  • 如果大小超过2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出窗口,直到哈希表的大小小于等于2,然后更新结果;
  • 如果没有超过2:说明当前窗口内水果的种类不超过两种,直接更新结果ret。
     

 有了思路,画图独立完成代码,不要直接看博主的代码。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int, int> hash;
        int left = 0, right = 0;
        int ret = 0;
        int n = fruits.size();
        while (right < n)
        {
            hash[fruits[right]]++;//进入窗口
            while (hash.size() > 2)//判断
            {
                hash[fruits[left]]--;//离开窗口
                if (hash[fruits[left]] == 0)
                {
                    hash.erase(fruits[left]);
                }
                left++;
            }
            ret = max(ret, right - left + 1);//更新结果
            right++;
        }
        return ret;
    }
};

但如果使用容器,我们需要频繁地erase元素,这就牺牲了一定的时间。

又因为题目说明元素个数是有限的:

​所以我们可以利用数组模拟一个哈希表,这样效率会显著提升。 

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int hash[100001]={0};
        int left=0,right=0,kinds=0;
        int ret=0;
        int n=fruits.size();
        while(right<n)
        {
            if(hash[fruits[right]]==0) kinds++;
            hash[fruits[right]]++;//进入窗口
            while(kinds>2)//判断
            {
                hash[fruits[left]]--;//离开窗口
                if(hash[fruits[left]]==0)
                    kinds--;
                left++;
            }
            ret=max(ret,right-left+1);//更新结果
            right++;
        }
        return ret;
    }
};


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

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)https://leetcode.cn/problems/find-all-anagrams-in-a-string/

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

不难发现,我们需要在字符串 s 中维护一个滑动窗口,且该滑动窗口的长度始终与字符串 p 相等。

然后依据该窗口内的元素构建哈希表与字符串 p 的哈希表作比较,如果两个哈希表相同,那么就证明滑动窗口内为字符串 p 的异位词。

那么如何比较两个哈希表是否相同呢?如题意:

​字符串 s 和 p 仅包含小写字母,所以我们只要遍历即可,时间复杂度为常数级,可以忽略。 

有了思路,画图独立完成代码,不要直接看博主的代码。 

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = { 0 };
        for (auto e : p)
            hash1[e - 'a']++;
        int hash2[26] = { 0 };
        int left = 0, right = 0;
        int np = p.size();
        int ns = s.size();
        vector<int> ret;
        while (right < ns)
        {
            hash2[s[right] - 'a']++;//进入窗口
            if (right - left + 1 > np)//判断
                hash2[s[left++] - 'a']--;//离开窗口
            int flag = 0;
            for (int i = 0; i < 26; i++)
                if (hash1[i] != hash2[i])
                    flag = 1;
            if (flag == 0)
                ret.push_back(left);//更新结果
            right++;
        }
        return ret;
    }
};

可是如果 s 和 p 内不光存储小写字母,或者 s 和 p 是某种容器存储的是字符串,我们又该如何处理呢?如果还按照遍历的方式显然不现实,所以我们需要引入『 有效字符计数器count』。

  • 在每次『 进入窗口』之后,要维护count的值:如果该进入窗口的字符在 s哈希表 中的数目小于或等于 p哈希表 中的数目,那么就证明此时进入窗口的字符是 有效字符,count++;
  • 在每次『 离开窗口』之前,要维护count的值:如果该离开窗口的字符在 s哈希表 中的数目小于或等于 p哈希表 中的数目,那么就证明要离开窗口的字符是 有效字符,count--;
  • 每轮如果 有效字符数目与 p字符串长度相等,那么就证明此时 s字符串窗口内是 p字符串 的异位词,将left尾插到vector中。
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = { 0 };
        for (auto e : p)
            hash1[e - 'a']++;
        int hash2[26] = { 0 };
        int left = 0, right = 0, count = 0;
        int np = p.size();
        int ns = s.size();
        vector<int> ret;
        while (right < ns)
        {
            char in = s[right];
            if (++hash2[in - 'a'] <= hash1[in - 'a']) count++;//进入窗口后维护count
            if (right - left + 1 > np)//判断
            {
                char out = s[left++];
                if (hash2[out - 'a']-- <= hash1[out - 'a']) count--;//离开窗口前维护count
            }
            if (count == np)
                ret.push_back(left);//更新结果
            right++;
        }
        return ret;
    }
};

7.串联所有单词的子串(hard) 

30. 串联所有单词的子串 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/substring-with-concatenation-of-all-words/

给定一个字符串 s 和一个字符串数组 words words 中所有字符串 长度相同

 s 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd""cdabef", "cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

其实本题就是第6题的升级版,只不过第6题的原子是字符,本题的原子是字符串,因为给定的容器words内存储的字符串都是等长的。

所以整体的思路与第6题是完全相同的,只不过需要处理一些细节。


细节一:

执行一次滑动窗口逻辑不能包括所有情况,因为我们把字符串看作一个原子处理,但是字符串由一个个字符构成,一个字符串内的部分字符可以和另一个字符串的部分字符组成新的字符串,所以我们需要充分考虑所有情况,经过观察发现我们需要执行 字符串原子的长度次len 就能包含所有情况,比如:

这反映到left和right开始的位置。 


细节二:

结束条件应为 right + 原子字符串长度len > 字符串长度 。

因为如果大于,right再往后就够不成原子字符串了。


 细节三:

right 与 left 每次移动 原子字符串长度len,而不是1。


细节四:

『 判断』条件应为 right - left + 1 > 原子字符串长度len *  words中的字符串个数。


有了思路,画图独立完成代码,不要直接看博主的代码。 

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        unordered_map<string,int> hash1;
        for(auto& e: words)
            hash1[e]++;
        
        int ns=s.size();
        int nw=words.size();
        int len=words[0].size();
        vector<int> ret;
        for(int i=0;i<len;i++)//细节1
        {
            int left=i,right=i,count=0;
            unordered_map<string,int> hash2;//维护窗口内单词的频次
            while(right + len <= ns)//细节2
            {
                //进入窗口+维护count
                string in = s.substr(right,len);
                hash2[in]++;
                if(hash2[in]<=hash1[in]) count++;

                //判断
                if(right-left+1 > len*nw) //细节4
                {
                    //离开窗口+维护count
                    string out=s.substr(left,len);
                    if(hash2[out]<=hash1[out]) count--;
                    hash2[out]--;
                    left+=len;//细节3
                }

                if(count==nw)
                {
                    ret.push_back(left);//更新结果
                }
                right+=len;//细节3
            }
        }
        return ret;
    }
};

到这代码还能进一步优化。


细节五:

维护count时,因为判断语句会被执行,所以如果进入窗口的字符串in在hash1中不存在,那么in这个字符串就会加入到hash1中,这无疑是一种浪费,所以在比较之前,我们可以判断一下in是否在hash1中,如果不在那也就没有比较的必要了,out那块同理。

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        unordered_map<string,int> hash1;
        for(auto& e: words)
            hash1[e]++;
        
        int ns=s.size();
        int nw=words.size();
        int len=words[0].size();
        vector<int> ret;
        for(int i=0;i<len;i++)//细节1
        {
            int left=i,right=i,count=0;
            unordered_map<string,int> hash2;//维护窗口内单词的频次
            while(right + len <= ns)//细节2
            {
                //进入窗口+维护count
                string in = s.substr(right,len);
                hash2[in]++;
                if(hash1.count(in) && hash2[in]<=hash1[in]) count++;//细节5

                //判断
                if(right-left+1 > len*nw) //细节4
                {
                    //离开窗口+维护count
                    string out=s.substr(left,len);
                    if(hash1.count(out) && hash2[out]<=hash1[out]) count--;//细节5
                    hash2[out]--;
                    left+=len;//细节3
                }

                if(count==nw)
                {
                    ret.push_back(left);//更新结果
                }
                right+=len;//细节3
            }
        }
        return ret;
    }
};

8.最小覆盖字串(hard)

76. 最小覆盖子串 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/minimum-window-substring/description/

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

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

 同样的我们最先想到暴力枚举+哈希表的办法,但我们可以观察得到一定的规律做优化。


第一个问题:

当right右移到满足覆盖的条件时,left左移,right是否需要回退呢?

其实不需要,因为中间的元素我们是知道的,只需要在left左移时判断right是否需要移动即可。

  • 当left右移后,如果窗口内还满足覆盖条件,那么就证明right此时可以不动;
  • 当left右移后,如果窗口内不满足覆盖条件,那么就证明right要右移寻找新的满足条件的字符。

 而且根究上面的进出窗口,我们可以知道出窗口之前,即left右移之前,此时窗口内是满足条件的字符串,所以『 更新结果』要在『 离开窗口』之前完成。

并且如果窗口内一直满足覆盖条件,那么就应该一直出窗口,直到不满足覆盖条件为止,所以这里应该用while。


第二个问题:

如何判断是否满足覆盖条件,我们之前说利用哈希表,但两个哈希表又如何判断相等呢?

之前的题目相信对你有所启发,我们是利用了一个 有效字符计数器count 来判断,在这里我们同样可以利用这个 count,但唯一不同的是,此 count 要统计的是字符种类,而不是字符数,因为题目说了,对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

所以进入窗口之后维护count的条件,应该是哈希表中对应字符的个数相等,此时才证明恰好覆盖。


 有了思路,画图独立完成代码,不要直接看博主的代码。 

class Solution {
public:
    string minWindow(string s, string t) {
        int hash1[128]={0};
        int kinds=0;
        for(auto ch : t)
        {
            if(hash1[ch]==0)//统计有效字符有多少种
                kinds++;
            hash1[ch]++;
        }

        int left=0,right=0,count=0;
        int ns=s.size();
        int nt=t.size();
        int hash2[128]={0};

        int minLen=INT_MAX,begin=-1;

        while(right<ns)
        {
            //进入窗口+维护count
            char in=s[right];
            if(++hash2[in]==hash1[in]) count++;

            while(count==kinds)//判断
            {
                if(right-left+1 <minLen)//更新结果
                {
                    minLen=right-left+1;
                    begin=left;
                }
                //离开窗口+维护count
                char out=s[left++];
                if(hash2[out]==hash1[out]) count--;
                hash2[out]--;
            }
            right++;
        }
        if(begin==-1) return "";
        else return s.substr(begin,minLen);
    }
};

滑动窗口专题结束,接下来是『 二分算法』


=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1456241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jq 图片懒加载 + Vue-Lazyload

jq原生 图片 懒加载 <!DOCTYPE html> <html lang"zh-cn"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv"X-UA-Compati…

《2024巨量引擎日化行业白皮书》丨附下载

✦ ✦✦ ✦✦ ✦✦ ✦ 中国日化行业在2022年短暂承压之后&#xff0c;随着生活恢复常态&#xff0c;迎来新的发展契机&#xff0c;2023年呈回稳向上态势。以抖音为代表的内容电商是行业增长的主要驱动力&#xff0c;内容场和货架场互通互联&#xff0c;促进行业全域化释放潜能…

从零开始手写mmo游戏从框架到爆炸(十二)— 角色设定

导航&#xff1a;从零开始手写mmo游戏从框架到爆炸&#xff08;零&#xff09;—— 导航-CSDN博客 写了这么多的框架&#xff0c;说好的mmo游戏呢&#xff1f;所以我们暂时按下框架不表&#xff0c;这几篇我们设计英雄角色、怪物、技能和地图。本篇我们来对游戏角色…

解决Webstorm2023使用账号连接GitLab的问题personal access token instead of a password

问题 升级Webstorm之后&#xff0c;发现gitlab仓库拉取代码报错 报错信息 remote: HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See https…

Docker Desktop 链接windos 安装的redis和mysql

1.1.先在容器安装项目 2.链接redis和mysql配置 redis和mysql是在windos安装的&#xff0c;使用的是小p管理器安装的 项目链接 DB_DRIVERmysql DB_HOSThost.docker.internal DB_PORT3306 DB_DATABASEyunxc_test DB_USERNAMEyunxc_test DB_PASSWORDtest123456... DB_CHARSETutf…

java.sql.SQLException: No operations allowed after statement closed.

背景 某天下午&#xff0c;客服反馈线上服务出现问题&#xff0c;不能分配了。于是我登录到系统上&#xff0c;进行同样的操作发现也不行。当然同时我已经登录到服务器打开了日志&#xff0c;发现报错了&#xff0c;下面就是日志的错误信息&#xff1a; java.sql.SQLExceptio…

【开源】JAVA+Vue.js实现农村物流配送系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统登录、注册界面2.2 系统功能2.2.1 快递信息管理&#xff1a;2.2.2 位置信息管理&#xff1a;2.2.3 配送人员分配&#xff1a;2.2.4 路线规划&#xff1a;2.2.5 个人中心&#xff1a;2.2.6 退换快递处理&#xff1a;…

Java入门基础语法

文章目录 3.1 字面量3.2 数据类型3.3 变量3.4 变量的案例3.4.1 手机信息描述3.4.2 疫情防控信息采集表 3.5 变量的注意事项3.6 关键字3.7 标识符 来学习 Java 基础语法部分的知识&#xff0c;这些内容是我们后面编写程序的基本功&#xff0c;所以呢&#xff0c;得好好学习&…

C/C++数据结构——剖析排序算法

1. 排序的概念及其运用 1.1 排序的概念 https://en.wikipedia.org/wiki/Insertion_sorthttps://en.wikipedia.org/wiki/Insertion_sort 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的…

MySQL篇之主从同步原理

一、原理 MySQL主从复制的核心就是二进制日志。 二进制日志&#xff08;BINLOG&#xff09;记录了所有的 DDL&#xff08;数据定义语言&#xff09;语句和 DML&#xff08;数据操纵语言&#xff09;语句&#xff0c;但不包括数据查询&#xff08;SELECT、SHOW&#xff09;语句。…

【开源】SpringBoot框架开发教学过程管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

DS:八大排序之归并排序、计数排序

创作不易&#xff0c;感谢三连支持&#xff01;&#xff01; 一、归并排序 1.1 思想 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。将已有序的子…

序列发生器

一开始想直接FSM&#xff0c;划分出6状态依次输出对应的。但其实只要6比特的移位寄存器&#xff0c;每次输出高位。复位后的默认值时6’b001_011。这样就可以实现循环&#xff0c;这种移位寄存器也叫barrel_shifter。循环移位。也可以使用循环计数器&#xff0c;然后case计数器…

机试指南:3-4章

文章目录 第3章 排序与查找(一) 排序1.sort函数&#xff1a;sort(first,last,comp)2.自定义比较规则3.C函数重载&#xff1a;同一个函数名&#xff0c;有不同的参数列表4.机试考试最重要的事情&#xff1a;能把你曾经做过的题目&#xff0c;满分地做出来5.例题例题1&#xff1a…

找座位 - 华为OD统一考试(C卷)

OD统一考试(C卷) 分值: 100分 题解: Java / Python / C++ 题目描述 在一个大型体育场内举办了一场大型活动,由于疫情防控的需要,要求每位观众的必须间隔至少一个空位才允许落座。 现在给出一排观众座位分布图,座位中存在已落座的观众,请计算出,在不移动现有观众座位…

全国工商企业名录

全国2023年12月份企业名录2.5亿条

成功交易者需要历经多少磨难才能成就辉煌?

前言 王国维在《人间词话》中说&#xff1a;古今之成大事业、大学问者&#xff0c;必经过三种之境界&#xff1a;“昨夜西风凋碧树&#xff0c;独上高楼&#xff0c;望尽天涯路。” 此第一境也。“ 衣带渐宽终不悔&#xff0c;为伊消得人憔悴。” 此第二境也。“众里寻他千百度…

2023年【四川省安全员B证】最新解析及四川省安全员B证新版试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年四川省安全员B证最新解析为正在备考四川省安全员B证操作证的学员准备的理论考试专题&#xff0c;每个月更新的四川省安全员B证新版试题祝您顺利通过四川省安全员B证考试。 1、【多选题】《建筑施工安全检查标准…

振弦采集仪在地铁隧道振动监测中的应用研究

振弦采集仪在地铁隧道振动监测中的应用研究 河北稳控科技振弦采集仪是一种用于测量振动信号的设备&#xff0c;广泛应用于结构和地铁隧道等工程监测中。在地铁隧道振动监测中&#xff0c;振弦采集仪可以提供以下方面的应用研究&#xff1a; 1. 地铁隧道振动监测&#xff1a;振…

测试演示文档1770532693631507859

测试录入文档 这是一个标题 正文正文 段落1段落2 随机数YY 第二张图 XXXY ct-1708314487159)] XXXY