【Hello Algorithm】滑动窗口内最大值最小值

简介: 【Hello Algorithm】滑动窗口内最大值最小值

滑动窗口介绍

滑动窗口是一种我们想象中的数据结构 它是用来解决算法问题的

1da29584d79541cb9dd2477885f6a27f.png我们可以想象出一个数组 然后再在这个数组的起始位置想象出两个指针 L 和 R


7bf9fe936d3b424b82cc749ccc935109.png

我们对于这两个指针做出以下规定

  • L 和 R指针只能往右移动
  • L指针不能走到R指针的右边
  • 我们只能看到L指针和R指针中间的数字

比如说当前L和R指针重合 我们就什么数字都看不见

如果此时R指针往右走一步 那么我们就能看到L指针和R指针中间的数组的数字

又因为这种移动的方式特别像滑动 所以说我们将这种想象出来的数据结构叫做滑动窗口

如何确定一个滑动窗口内的最大值

如果我们每次都遍历去获取滑动窗口的最大值的话那么每次获取的时间复杂度就是O(N)了 这样子明显很复杂 所以说我们需要想想别的方式去找到最大值

这里直接给出结论:

  • 我们使用双端队列去获取一个滑动窗口内的最大值
  • 双端队列的意义是 : 此时开始缩小滑动窗口 哪些数字可能成为最大值

为了让双端队列能够实现它的意义 我们做出以下规定

  • 让R指针向右滑动的时候 我们从右边插入数字的下标到双端队列中
  • 如果说插入的数字要大于原来的数字 我们让原来的数字出队列
  • 让L指针向右滑动的时候 我们从左边开始比较双端队列第一个(左边数起)下标和L指针的下标
  • 如果说L指针的下标要大于双端队列最左边的下标 则将其弹出

至于C++中的双端队列 大家可以参考这篇博客 双端队列

窗口内最大值

假设一个固定大小为W的窗口 依次划过数组arr

让你依次返回每次窗口移动时窗口中的最大值

假设数组arr为 【4 , 3 , 5 ,4, 3, 3, 6, 7】 W = 3

返回【5 , 5, 5, 4, 6, 7】


这道题目实际上就是一个滑动窗口最大值的简单版本

我们使用一个双端队列就能很轻松的实现

面对这个问题我们可以拆分成两部分来解决

  1. 首先把滑动窗口的大小扩大到3
  2. 接着滑动窗口整体开始移动

两部分的代码都不算难 代码表示如下

void process5(vector<int>& arr , vector<int>& ans , int W)
{
  deque<int> dq(0  , 0);
  for (int R = 0; R < W; R++)
  {
    while (!dq.empty() && arr[dq.front()] <= arr[R])
    {
      dq.pop_back();
    }    
    dq.push_back(R);    
  }    
  int R = W - 1;    
  int L = 0;    
  int N = static_cast<int>(arr.size());    
  while (R < N)    
  {    
    while (!dq.empty() && arr[dq.back()] <= arr[R])                                                                                                                          { 
        dq.pop_back();
    }                     
    dq.push_back(R);
    while (L > dq.front())    
    {                         
       dq.pop_front();
    }                     
    ans.push_back(arr[dq.front()]);
    L++;    
    R++;
  }
}

这道题目给我们的提醒主要有两个

  1. 当我们R指针右移的时候要使用while来进行判断
  2. R指针在初始化之后要重置 否则会出现一些错误 为了避免这些错误 我们最好每次使用一个变量之前对其进行初始化

子数组中符合条件的个数

给定一个整形数组arr 和一个整数num

某个arr中的子数组sub 必须要满足

sub中的最大值减去sub中的最小值小于等于num

返回sub中达标的子数组的数量

我们首先分析下问题

如果我们没有学过滑动窗口和双端队列 那么这道题我们会使用暴力的方式来得到答案

三个for循环嵌套 时间复杂度就变成了n的三次方 这显然是不可以的

当我们使用滑动窗口解决这个问题的时候 由于我们的左右两个下标都是单向的往右边移动 所以说此时的时间复杂度就是N


当我们得到两个下标满足上述条件时 比如说0 ~ N上的最大值减去0~N中的最小值小于等于num

此时我们就可以说 左下标为0 右下标为0 ~ N-1上的任意一个子数组都满足条件

因为此时最大值只有可能变小 最小值只有可能变大 所以说它们之间的差值肯定还是会小于等于num

所以我们就能确定 以0为左边界 N为右边界上符合条件的子数组有 N - 0 + 1个

之后我们左边界右移即可


整体思路如下

  • 我们使用两个双端队列来记录子数组的最大值和最小值
  • 我们让右边界一直右移动 直到子数组不满足条件为止
  • 此时通过上面的技巧计算出左边界到右边界-1的满足条件子数组个数 之后左边界++

代码表示如下

int process(vector<int>& arr, int num)
{
    deque<int> Max_Win(0, 0);
    deque<int> Min_Win(0, 0);
    int count = 0;
    int R = 0;
    int N = static_cast<int>(arr.size());
    for (int L = 0; L < N; L++)
    {
        while (R < N)
        {
            while (!Max_Win.empty() && arr[Max_Win.back()] <= arr[R])
            {
                Max_Win.pop_back();
            }
            Max_Win.push_back(R);
            while (!Min_Win.empty() && arr[Min_Win.back()] >= arr[R])
            {
                Min_Win.pop_back();
            }
            Min_Win.push_back(R);
            if (arr[Max_Win.front()] - arr[Min_Win.front()] > num)
            {
                break;
            }
            else
            {
                R++;
            }
        }
        count += R - L;
        if (Max_Win.front() == L)
        {
            Max_Win.pop_front();
        }
        if (Min_Win.front() == L)
        {
            Min_Win.pop_front();
        }
    }
    return count;
}

这里有一点需要注意的是

count += R - L;

语句必须要放到while循环的外面才行 否则会因为R下标越界的问题而导致不会执行if语句 最终导致count计算不完整

加油站的良好出发点问题

此时我们有两个数组 gas 和 cost

gas数组的意思每个加油点可以加多少单位的油

cost数组的意思是从当前加油点到下个加油点需要花费多少油(如果下个加油点不存在则视为是第一个加油点)

问题是 假设从其中任意一个加油点触发 起始车里没有油 能否跑完一圈?


我们可以稍微理解下这个问题 起始两个数组gas 和 cost 可以转化为一个 rest 数组

如果 rest 数组中有小于0的值则说明从该出发点无法跑通 如果没有则说明可以跑通

其实我们想到了这一层就应该能联想到要使用窗口内最小值来解这道题目了 但是这里又会出现一个新的问题 – 如果我们的起点不是从0下标开始的 那么整个数组就需要 “拐弯”

对于这个问题的解决方式是 我们可以将整个数组扩容到原来的双倍

根据原来的数组 我们可以建立一个新的数组并且将每个数组位置的含义变为汽车到该加油站时剩余的单位油量 超出原数组的部分我们从0下标开始再顺序循环一遍

这样子我们就可以使用滑动窗口来解决这个问题了 解决步骤如下

  • 设置原数组的大小为窗口大小
  • 每次窗口移时选取一个最小值

代码表示如下

void process(vector<int>& arr , int num , vector<int>& ans)
{
  deque<int> Min_Win(0 , 0);
  for (int R = 0; R < num ; R++)
  {
    while(!Min_Win.empty() && arr[Min_Win.back()] >= arr[R])
    {
      Min_Win.pop_back();
    }
    Min_Win.push_back(R);
  }                    
  int R = num - 1;   
  int L = 0;        
  while (R < static_cast<int>(arr.size()))  
  {  
   while(!Min_Win.empty() && arr[Min_Win.back()] >= arr[R])  
    {                         
        Min_Win.pop_back();  
    }                             
    Min_Win.push_back(R);                                     
    if (Min_Win.front() == L)  
    {  
      Min_Win.pop_front();  
    }                      
    L++;  
    R++;  
    if (arr[Min_Win.front()] < 0)  
    {                                       
      ans.push_back(0);  
    }  
    else                                                         
    {      
      ans.push_back(1);                                                                                                                                                                                                                                             
    }                                                                                                                                  
  }                                                                                                                                                                                                                                                                          
}   

之后我们只需要对于vector里面的int类型数据进行判断 如果是0则不能到达 如果是1则可以到达

不过这里也有几个需要注意的问题

  1. 我们不能使用 vector<bool> 来存储bool类型的数据
  2. 在传入porcess函数之前我们需要对于数组进行加工
  1. 每次使用L , R下标之前需要重置成我们希望的值 否则可能会出现一些问题

滑动窗口问题总结

当我们需要一段范围的最大值或者是最小值的时候 我们能够想到两种方式

  • 堆排序
  • 滑动窗口

而什么时候选择排序什么时候选择滑动窗口呢?

我们说 当数据的范围在不停的变化(这个变化要是线性的 不能是随机的)的时候 我们可以选择滑动窗口来解决我们的问题

当只有数据增加的时候我们可以选择使用堆排序来解决问题

相关文章
|
网络安全 开发工具 git
|
数据采集 存储 XML
Python爬虫:深入探索1688关键词接口获取之道
在数字化经济中,数据尤其在电商领域的价值日益凸显。1688作为中国领先的B2B平台,其关键词接口对商家至关重要。本文介绍如何通过Python爬虫技术,合法合规地获取1688关键词接口,助力商家洞察市场趋势,优化营销策略。
|
数据安全/隐私保护 开发者 计算机视觉
《鸿蒙 HarmonyOS 应用开发从入门到精通(第 2 版)》学习笔记 ——HarmonyOS 环境搭建之注册华为开发者联盟帐号
要进行HarmonyOS应用开发,首先需要注册华为开发者联盟帐号并完成实名认证。注册时可选择成为个人或企业开发者,两者享有不同权益。个人开发者需准备手机号/邮箱、身份证扫描件及银行卡号等资料,通过审核后即可享受应用市场、主题、商品管理等多项服务。具体步骤包括访问华为开发者官网(https://developer.huawei.com/consumer/cn/),选择注册方式并按指引操作。实名认证需填写个人信息并签署相关协议,等待1-3个工作日的审核结果。
2215 16
|
传感器 芯片
STM32外设系列—HC-SR04(超声波)
本文主要介绍了超声波测距的原理,常用的超声波传感器。并且针对HC-SR04给出了使用思路和程序设计。最后,简单进行了思路拓展。
997 1
STM32外设系列—HC-SR04(超声波)
|
机器学习/深度学习 PyTorch API
ONNX 与实时应用:延迟敏感场景下的部署策略
【8月更文第27天】在实时应用中,如自动驾驶汽车、视频分析系统等,快速响应和高吞吐量是至关重要的。Open Neural Network Exchange (ONNX) 提供了一种标准化的方法来部署机器学习模型,使其能够在不同的硬件和平台上高效运行。本文将探讨如何利用 ONNX 在延迟敏感的应用场景中部署模型,并提供一些策略和示例代码来确保低延迟和高吞吐量。
1775 4
官宣!杭州市地铁集团与阿里云达成战略合作
官宣!杭州市地铁集团与阿里云达成战略合作
1045 9
|
监控 数据中心
【专栏】交换机电口和光口的定义、特点及应用场景,做网络的这个常识得懂!
【4月更文挑战第28天】本文探讨了交换机电口和光口的定义、特点及应用场景。电口,常见于局域网和办公环境,成本低但传输距离有限;光口,适用于长距离、高速率传输,如城域网、数据中心,具有抗干扰强但成本高的特点。选择接口时需考虑传输距离、速率和成本,注意兼容性、线缆选择及维护管理。理解两者差异有助于网络规划和管理。
1657 0
|
网络协议 算法 数据库
IS-IS原理与配置
IS-IS原理与配置
|
运维 监控 网络协议
端口号大揭秘:网络世界的“门牌号”有多牛?
端口号大揭秘:网络世界的“门牌号”有多牛?
1044 0
|
JSON JavaScript 前端开发
Vue 3.3 + Vite 4.3 + TypeScript 5+ Element-Plus:从零到一构建企业级后台管理系统(前后端开源)(三)
Vue 3.3 + Vite 4.3 + TypeScript 5+ Element-Plus:从零到一构建企业级后台管理系统(前后端开源)(三)

热门文章

最新文章