【看海的算法日记✨优选篇✨】第二回:流动之窗,探索算法的优雅之道

🌈 个人主页:谁在夜里看海.

🔥 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 道阻且长,行则将至


目录

一、算法思想

双指针

滑动窗口

二、具体运用

1.⻓度最⼩的⼦数组

算法思路

算法流程

代码

2.最⼤连续1的个数III

算法思路

算法流程

代码

3.⽔果成篮

算法思路

算法流程

代码

4.串联所有单词的⼦串

算法思路

算法流程

代码

三、总结


一、算法思想

上一章我们介绍了双指针的算法思想,其核心原理是利用两个指针在数组中移动,以高效解决一个涉及有序序列、子区间的问题。我们主要介绍的是用于解决在有序区间中判断元素对的场景,即判断两个指针指向的元素,并利用序列的单调性完成指针的移动。

我们今天要讲的滑动窗口本质是双指针算法的特化,它与上一章所讲的双指针有所区别:

特性双指针滑动窗口
移动方向两个指针可以同向移动或对向移动只能同向移动,窗口随之调整
窗口特性不一定维护特定的窗口,只关注指针的状态维护一个动态窗口,用于表示当前连续区间
使用场景常用于查找元素对,判断满足条件常用于处理连续子数组或子字符串的问题
条件变化两个指针的关系由问题决定,没有固定结构窗口大小动态变化,与目标条件有关

双指针

滑动窗口

在每次调整窗口后,判断窗口是否满足条件,如果能满足,更新结果以记录最优解。

只看算法原理可能又有些抽象,下面看看具体的使用场景:

二、具体运用

1.⻓度最⼩的⼦数组

难度等级:⭐⭐⭐⭐

题目链接:209. 长度最小的子数组 - 力扣(LeetCode)

题目描述:

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 

子数组

  [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度 如果不存在符合条件的子数组,返回  0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
算法思路

暴力枚举:

这道题目的暴力枚举思路是,把每一个区间都枚举出来,筛选出元素总和满足条件的,然后再进一步筛选区间长度最短的,就是最终结果。枚举全部区间我们首先想到的是用两层for循环,但那样时间复杂度太高了,所以我们选择用双指针进行优化:定义left、right指针,用指针的移动完成对数组的遍历

遍历的过程为:固定left指针为区间的起始位置,right向后一次遍历,枚举出当前起始位置下的所有区间,这里我们要想一个问题,right指针需要将所有元素都遍历一遍吗?并不需要,我们可以在right遍历的过程中,实时地记录当前区间元素和,当元素和>target(满足条件)时,right就不需要要继续遍历了,因为题目要求的是区间长度最小的,再遍历下去长度只会增大。于是我们的滑动窗口思想就产生了:

滑动窗口:

延续上面的思路,固定left作为区间起始位置,right在遍历的过程中找到当前起始位置下满足题目条件(区间元素和>target)的情况时停止遍历,那么接下来应该怎么做呢?移动left指针,修改区间的起始位置,right指针需要从起始位置开始重新遍历吗?并不需要,移动left指针之前我们找的的区间是刚好满足题目条件的情况,此时移动left指针,区间元素减少,那么这一段区间肯定都是不满足条件的,所以right指针不需要回到起始位置,从当前位置继续向后遍历即可。

算法流程

①:定义left、right指针,初始都指向数组首元素;

②:固定left指针,作为区间起始位置,right从起始位置开始向后遍历,遍历的过程中记录区间内元素总和,当元素总和>target时,停止遍历,记录当前区间长度;

③:移动left指针,改变区间起始位置,right不需要回到起始位置,直接从当前位置向后遍历,后续操作与步骤②一样;

④:最后找到满足条件的最短区间情况。

代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left = 0,right = 0,sum = nums[right],len = INT_MAX;
        while(right<nums.size())
        {
            if(sum<target)
            {
                ++right;
                if(right<nums.size()) sum+=nums[right];
            }
            else
            {
                if(left == right) return 1;
                len = min(len,right-left+1);
                sum-=nums[left];
                ++left;
            }
        }
        return len == INT_MAX ? 0:len;
    }
};

可以看到我们的时间复杂度是非常优秀的

2.最⼤连续1的个数III

难度等级:⭐⭐⭐⭐

题目链接:1004. 最大连续1的个数 III - 力扣(LeetCode)

题目描述:

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。
算法思路

这道题也是让我们找一个满足条件的区间,与上一题类似,有了上一题的铺垫,这道题我们直接从滑动窗口的思想入手:

首先我们还是确定区间起始位置,下一步需要考虑是right遍历的终止情况,即right何时停止遍历。题目让我们找的是连续1的最长区间,并且告诉我们,区间内允许出现k个0元素,所以我们right终止遍历的情况就是:遇到0元素且翻转次数k用完,此时就是当前起始位置下的最长区间

后面的操作就是移动left指针,修改区间起始位置,那么left是移动一次吗?并不是,如果只移动一次,并不能支持right向后遍历,因为k没有增加,而right当前指向的是0元素,所以left需要一直移动直到将一个0元素“挤出”区间,此时k才会增加,right才能继续遍历。

算法流程

①:初始化left、right指针,指向数组首元素;

②:固定left,作为区间起始位置,right向后遍历,并实时记录当前k的使用情况,遇到0时k--,当k减到0时并且right来到下一个0元素之前,停止遍历,记录当前区间长度;

③:移动left,修改区间起始位置,left需要排出一个0元素方可停止移动,然后right继续遍历;

④:最后找到满足条件的最长区间。

代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0,right = 0,flag = k,num = 0,ret = 0;
        for(right;right<nums.size();++right)
        {
            // 为1,继续遍历
            if(nums[right])
            {
                ++num;
                ret = max(num,ret);
            }
            // 为0,减小计数
            else
            {
                --flag;
                if(flag>=0)
                {
                    ++num;
                    ret = ret>num?ret:num;
                    continue;
                }

                // 计数用完,移动left直到跳过一个0,返还一个计数
                while(nums[left])
                {
                    ++left;--num;
                }
                ++left;++flag;
            }
        }
        return ret;
    }
};

3.⽔果成篮

难度等级:⭐⭐⭐⭐

题目链接:904. 水果成篮 - 力扣(LeetCode)

题目描述:

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

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

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

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

示例 1:

输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:

输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
算法思路

题目描述很长,简单概括一下就是:在数组中找到一个连续区间,区间内只能包含两种元素,筛选出满足条件的最长区间

既然是寻找连续区间,那还是用滑动窗口进行解决:

首先依旧是固定区间起始位置,用right指针向后遍历,遍历的终止情况是,区间内已经包含两种元素,并且right位置的下一个元素是第三种元素,我们可以用一个type变量监控当前区间的元素种类

然后是left的移动,修改区间起始位置,同样地left的移动需要将一种元素全部“排除”区间,使type+1,right方可继续遍历。

算法流程

①:初始化left、right,指向数组首元素;

②:固定left,作为区间起始位置,right向后遍历,遇到一种新元素,type--,直到type减到0并且right来到下一种元素前一个位置;

③:移动left,修改区间起始位置,left的移动需要排出全部的一种元素,right才能继续遍历;

④:最终筛选出满足条件的最长区间。

代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int left = 0,right=0,hash[100000]={0},type=0,ret=0;
        while(right<fruits.size())
        {
            if(hash[fruits[right]]==0) ++type;
            ++hash[fruits[right]];++right;
            if(type == 3)
            {
                ret=max(ret,right-left-1);
                // 移动窗口 直到type-1
                while(type == 3)
                {
                    --hash[fruits[left]];
                    if(!hash[fruits[left]]) --type;
                    ++left;
                }
            }
        }
        if(type==1) return fruits.size();
        if(type==2) return max(ret,right-left);
        return ret;
    }
};

4.串联所有单词的⼦串

难度等级:⭐⭐⭐⭐⭐

题目链接:30. 串联所有单词的子串 - 力扣(LeetCode)

题目描述:

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

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

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

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

示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
算法思路

题目的要求是让我们在字符串中寻找一个连续区间, 区间内包含数组words中的全部字符串,找到满足条件的所有区间。

我们定义一个哈希表,用来存储数组words中的全部字符串,并定义一个count,用于记录区间内还需要包含元素的个数。right向后遍历一个字符串时,对该字符串进行判断,如果存在于哈希表中,--count。当前遍历的字符串不存在哈希表中时,需要终止遍历,当count减到0时,记录当前区间起始位置;

对于left的移动,我们需要分为两种情况讨论:

①:left==right,说明区间内部不包含元素,需要同时移动left与right

②:left<right,由于right遍历时,只有当前字符串存在于哈希表中,才会继续遍历,这样就保证了区间内元素都是符合条件的,所以此时left只需要向后移动排除一个字符串,count++,支持right继续向后遍历。

注意:

上述思路是存在缺陷的,因为left和right都是以字符串的长度向后遍历的,并不是依次遍历,所以这个过程会遗漏很多情况,我们需要考虑到这些情况,解决办法就是:对区间起始位置加上一个偏移量,如果字符串长度为3,那偏移量范围就是0~2

算法流程

①:初始化left、right,初始值为0+偏移量;

②:固定left,作为区间首元素,left以字符串长度向后遍历,判断当前字符串是否存在哈希表中,如果存在,count--;如果不存在,移动left,修改区间。当count减到0时,记录当前区间起始位置,并移动left,修改区间;

③:移动left分为两种情况:

        a.left==right,说明区间为空,left和right同时向后移动;

        b.left<right,说明区间存在元素,left向后移动一个字符串,count--,当前位置作为区间起始位置,right继续向后变量。

④:找到满足条件的所有区间。

代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        unordered_map<string,int> hash;
        for(auto it:words) ++hash[it];
        int len = words[0].size();
        vector<int> ret;
        for(int i = 0;i<len;++i)
        {
            unordered_map<string,int> target = hash;
            int left = i,right = i;
            int count = words.size();
            while(right<s.size())
            {
                string cur = s.substr(right,len);
                if(target.count(cur) && target[cur]>0)
                {
                    --target[cur]; --count;
                    right += len;
                }
                else
                {
                    if(left != right)
                    {
                        string del = s.substr(left,len);
                        ++target[del]; ++count;
                        left += len;
                    }
                    else
                    {
                        right += len;
                        left += len;
                    }
                }
                if(count == 0)
                {
                    ret.push_back(left);
                    string del = s.substr(left,len);
                    ++target[del]; ++count;
                    left += len;
                }
            }
        }
        return ret;
    }
};

三、总结

滑动窗口算法通过动态维护子区间的特性,在遍历的过程中高效地找到问题的解。它的核心思想是利用两个指针构建窗口,并根据问题的条件灵活调整窗口大小。相比暴力枚举,滑动窗口在优化时间复杂度方面有显著优势,非常适用于处理连续性问题,如子数组、子字符串及其变形的场景。


以上就是【优选算法篇·第二章:滑动窗口】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MicroBlaze软核开发(一):Hello World

实现功能&#xff1a;使用 MicroBlaze软核 串口打印 Hello World Vivado版本&#xff1a;2018.3 目录 MicroBlaze介绍 vivado部分&#xff1a; 一、新建工程 二、配置MicroBlaze 三、添加Uart串口IP 四、生成HDL文件编译 SDK部分&#xff1a; 一、导出硬件启动SDK 二、…

camera驱动开发(初学)

camera驱动开发 初学camera驱动开发 随笔记一下顺便整理一下初学的学习路线。内容来源于各大网站&#xff0c;大自然的搬运工。 一、camera基本概念 ①三路电压 camera包含的三路电压为模拟电压&#xff08;VCAMA&#xff09;&#xff0c;数字电压&#xff08;VCAMD&#x…

在Ubuntu22.04.5上安装Docker-CE

文章目录 1. 查看Ubuntu版本2. 安装Docker-CE2.1 安装必要的系统工具2.2 信任Docker的GPG公钥2.3 写入软件源信息2.4 安装Docker相关组件2.5 安装指定版本Docker-CE2.5.1 查找Docker-CE的版本2.5.2 安装指定版本Docker-CE 3. 启动与使用Docker3.1 启动Docker服务3.2 查看Docker…

C# 编程效率提升指南:掌握算数运算、循环与方法封装

在这篇文章中将带你深入探索C#中的几大关键技术点&#xff0c;从如何精准进行算数运算、灵活运用循环控制结构&#xff0c;到通过方法封装提升代码的复用性&#xff0c;再到正确使用可空类型避免潜在的空值引用异常&#xff0c;这些概念和技巧无一不是编写清晰、高效、健壮代码…

常见Linux命令(详解)

文章目录 常见Linux命令文件目录类命令pwd 打印当前目录的绝对路径ls 列出目录内容cd 切换路径mkdir 建立目录rmdir 删除目录touch 创建空文件cp 复制文件或目录rm 移除文件或者目录mv 移动文件与目录或重命名cat 查看文件内容more 文件分屏查看器less 分屏显示文件内容head 显…

html+css网页设计 旅游 马林旅行社3个页面

htmlcss网页设计 旅游 马林旅行社3个页面 网页作品代码简单&#xff0c;可使用任意HTML辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#…

【目标跟踪】AntiUAV600数据集详细介绍

AntiUAV600数据集的提出是为了适应真实场景&#xff0c;即无人机可能会随时随地出现和消失。目前提出的Anti-UAV任务都只是将其看做与跟踪其他目标一样的任务&#xff0c;没有结合现实情况考虑。 论文链接&#xff1a;https://arxiv.org/pdf/2306.15767https://arxiv.org/pdf/…

网络——HTTP与HTTPS三次握手和四次挥手

HTTP协议本身并不直接处理TCP连接的建立和关闭&#xff0c;这些是由底层的TCP协议来完成的。但是&#xff0c;由于HTTP通常运行在TCP之上&#xff0c;因此理解TCP的三次握手&#xff08;用于建立连接&#xff09;和四次挥手&#xff08;用于关闭连接&#xff09;对于理解HTTP通…

Issue id: AppLinkUrlError 应用intent-filter 配置深链接 URL 问题分析 | AndroidManifest

AndroidManifest.xml 配置文件中&#xff0c;对 activity 组件进行声明的时候&#xff0c;独立应用在 IDE 显示 intent-filter 报错&#xff0c;但不影响实际编译&#xff0c;因为是系统应用&#xff0c;肯定会有此 URL 的存在。 AOSP 源码&#xff1a; <activity android:…

Scala中的正则表达式

它是一种强大的文本处理工具&#xff0c;通过定义一系列的字符和操作符组合来描述这些模式。简单来说&#xff0c;它就像一种文本模式的“配方”。 package test9object test9_1 {//正则表达式def main(args: Array[String]): Unit {//定义一个正则表达式//1.[ab]:表示匹配一个…

《中型 Vue 项目:挑战与成长》

一、引言 在当今的前端开发领域&#xff0c;Vue 作为一款渐进式 JavaScript 框架&#xff0c;以其强大的功能和灵活性备受开发者青睐。对于中型 Vue 项目而言&#xff0c;其重要性不言而喻。中型 Vue 项目通常在功能复杂度和规模上介于小型项目和大型项目之间&#xff0c;既需要…

vscode插件 live-server配置https

背景&#xff1a;前端有时候需要在本地搭建https环境测试某些内容&#xff08;如https下访问http资源&#xff0c;下载&#xff09; 步骤&#xff1a; 1.vscode集成开发软件(应该所有前端开发同学都安装了&#xff0c;我用webstorm&#xff0c;vscode备用) 2.vscode安装live…

DBA面试题-1

面临失业&#xff0c;整理一下面试题&#xff0c;找下家继续搬砖 主要参考&#xff1a;https://www.csdn.net/?spm1001.2101.3001.4476 略有修改 一、mysql有哪些数据类型 1&#xff0c; 整形 tinyint,smallint,medumint,int,bigint&#xff1b;分别占用1字节、2字节、3字节…

「Mac畅玩鸿蒙与硬件43」UI互动应用篇20 - 闪烁按钮效果

本篇将带你实现一个带有闪烁动画的按钮交互效果。通过动态改变按钮颜色&#xff0c;用户可以在视觉上感受到按钮的闪烁效果&#xff0c;提升界面互动体验。 关键词 UI互动应用闪烁动画动态按钮状态管理用户交互 一、功能说明 闪烁按钮效果应用实现了一个动态交互功能&#xf…

「Mac畅玩鸿蒙与硬件40」UI互动应用篇17 - 照片墙布局

本篇将带你实现一个简单的照片墙布局应用&#xff0c;通过展示多张图片组成照片墙效果&#xff0c;用户可以点击图片查看其状态变化。 关键词 UI互动应用照片墙布局Grid 布局动态图片加载用户交互 一、功能说明 照片墙布局应用的特点&#xff1a; 动态加载多张图片组成网格布…

LabVIEW中“this VI‘s owning library is missing”错误及解决

问题描述 当加载或打开一个VI时&#xff0c;如果其所属的项目库未加载到内存&#xff0c;LabVIEW将提示错误&#xff1a;“this VIs owning library is missing”&#xff08;该VI的所属库不存在&#xff09;。 该问题通常发生在以下情况下&#xff1a; 项目库文件丢失或路径…

电子电气架构 --- 新四化对汽车电子的影响

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 所谓鸡汤&#xff0c;要么蛊惑你认命&#xff0c;要么怂恿你拼命&#xff0c;但都是回避问题的根源&…

etcd分布式存储系统快速入门指南

在分布式系统的复杂世界中&#xff0c;确保有效的数据管理至关重要。分布式可靠的键值存储在维护跨分布式环境的数据一致性和可伸缩性方面起着关键作用。 在这个全面的教程中&#xff0c;我们将深入研究etcd&#xff0c;这是一个开源的分布式键值存储。我们将探索其基本概念、特…

汽车IVI中控开发入门及进阶(三十五):架构QML App Architecture Best Practices

在Qt/QML工程的架构中,架构很重要,虽然本身它有分层,比如QML调用资源文件(图片等)显示GUI界面,后面的CPP文件实现界面逻辑,但是这个分类还有点粗。在实际开发中,界面逻辑也就是基于类cpp的实现,也开始使用各种面向对象的设计模式,实现更加优秀的开发架构,这点尤其在…

Java版-速通数组基础知识

一,单数组的双指针法 从两端到中间的双指针法 例如,对于字符串abc,我们要对字符串进行反转,将字符串反转为cba。 可以使用一个初始位置在头部的指针pLeft,另一个起始位置在尾部的指针pRight,将两个指针同时向中间移动。 当为奇数个数组的时候,两个指针在中位相遇,当为…