当前位置: 首页 > article >正文

滑动窗口最大值

239. 滑动窗口最大值 - 力扣(LeetCode)

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 

示例

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

输入:nums = [1], k = 1
输出:[1]

题解

分析

该题的解题思路其实很明确

  • 维护一个滑动窗口遍历数组
  • 每次计算滑动窗口中的最大值

但问题在于如何求滑动窗口中的最大值,因此问题就转换成了如何用最少的时间求一个序列的最大值

一般情况下我们有以下几种算法

  • 直接在序列中暴力遍历
  • 使用内置sort函数 

显然,外部使用滑动窗口遍历数组的时间已经达到了O(n),如果我们采取以上方式计算最大值,那么我们最终的算法时间复杂度必然已经达到了O(n^2)

再次观察,由于我们只需要获取序列的最大值,并不关心序列的顺序,我们要问:

有没有一种数据结构,可以每次将序列元素放进去之后,自动更新最大值,并将最大值放到头部呢?这样我们只需将滑动窗口的数据放到这个数据结构中,然再从这个数据结构的头部取出元素不就成功解决问题了么

而数据结构中恰好有一种数据结构——,可以完成这个任务

当我们将一个序列放进一个大顶堆时,堆会自动调整,将最大值放到堆顶,我们只需要获取堆顶的元素即可

因此,算法的思路就变成了

  • 维护一个滑动窗口,遍历数组中的元素
  • 每次将滑动窗口中的序列放进大顶堆中,大顶堆会自动进行调整,将序列的最大值放到堆顶

而我们知道

遍历数组的时间是O(n),而堆调整的时间是O(log n),因此整个算法的时间复杂度就变成了O(n*log n) 

这样我们就得到了以下两种解法

  • 维护一个大顶堆
  • 使用优先级队列

除此之外,我们完全可以自己维护一个单调队列,这个队列中的元素按照降序排序,队头的元素永远是序列的最大值,这样我们只需要每次从队头中取出元素即可,而从队头取出元素的时间为O(1),因此整个的时间复杂度是O(n)

接下来,我们就优先级队列和单调队列进行说明

优先级队列

首先我们需要知道的是

优先级队列的底层数据结构就是堆,因此我们使用优先级队列这种数据结构解题,本质上就是在使用堆

有关优先级队列的使用和介绍可参考

std::priority_queue - cppreference.com

算法思路其实和上述解释的过程是一样的

  • 维护滑动窗口,并将滑动窗口中的数值放到优先级队列中
  • 优先级队列底层默认使用的是大顶堆,因此会自动进行堆调整,将最大值放到堆顶
  • 故只需取出堆顶的元素即可 

但问题在于, 

  • 我们需要保证优先级队列中的数据永远是当前滑动窗口中的序列这样才能保证优先级队列中堆顶元素的最大值是当前滑动窗口的最大值
  • 因此我们需要在移动滑动窗口时,将上一个滑动窗口中的元素pop掉
  • 但优先级队列中只有堆顶元素是确定的,我们无法确定被pop的元素在队列中的位置

 但上述条件其实不需要完全满足,也就是说

  • 我们不必确定当前优先级队列中的所有元素都一定是滑动窗口中的元素
  • 我们只需要确保当前优先级队列中的队头元素一定是当前滑动窗口中的元素,而不是上一个滑动窗口中的元素即可

所以,

我们在push优先级队列元素的时候,需要将元素在nums中的下标位置也记录下来,用于确定优先级队列中堆顶的元素是否是当前滑动窗口中的元素

故整个代码如下

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        //优先级队列,维护大顶堆
        std::priority_queue<std::pair<int,int>> pq;
        std::vector<int> res;
        
        //先处理第一个滑动窗口
        for(int i=0;i<k;++i)
            pq.emplace(nums[i],i);
        res.emplace_back(pq.top().first);
        
        //接着遍历剩余的滑动窗口
        for(int i=k;i<nums.size();++i){
            pq.emplace(nums[i],i);
            //只要发现堆顶中元素不在当前滑动窗口中,就将其剔除掉
            while(pq.top().second<(i-k+1)){
                pq.pop();
            }
            res.emplace_back(pq.top().first);
        }

        return res;
    }
};

单调队列

首先,我们需要维护一个队列,并保证

  • 每次push进去元素时,最大值永远在队头

 因此,我们可以这样写

        void push(const int& val){
        //只要push的元素val比队尾的元素大,就将其pop掉,直到push的元素val能够放到队头
            while(!queue_.empty() && val>queue_.back())
                queue_.pop_back();
            queue_.emplace_back(val);
        }

假如现在有三个元素【1,-1,3】需要依次被push,则整个push过程如下图所示

而队列pop的过程较为简单,如果发现是需要被pop的元素,直接pop掉即可,如下所示

        void pop(const int& val){
            if(!queue_.empty() && val==queue_.front())
                queue_.pop_front();
        }

 基于这样的数据结构,于是我们每次只需获取队头元素,即可获取到当前序列的最大值

        int getFront() const {
            return queue_.front();
        }

完整的单调队列代码如下所示

    class Queue{
    public:
        void push(const int& val){
            while(!queue_.empty() && val>queue_.back())
                queue_.pop_back();
            queue_.emplace_back(val);
        }
        void pop(const int& val){
            if(!queue_.empty() && val==queue_.front())
                queue_.pop_front();
        }
        int getFront() const {
            return queue_.front();
        }
    private:
        std::deque<int> queue_;
    };

于是整个解题过程就顺利成章的变为

    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Queue queue;
        std::vector<int> res;
        
        //先处理第一个滑动窗口        
        for(int i=0;i<k;++i)
            queue.push(nums[i]);
        res.emplace_back(queue.getFront());
        
        //接下来移动滑动窗口,遍历数组,并将元素放入queue中
        for(int i=k;i<nums.size();++i){
            //将当前滑动窗口左侧边界的数值剔除掉queue,保证当前queue中维护是当前滑动窗口中的序列
            queue.pop(nums[i-k]);
            //将当前滑动窗口中的值push进queue中
            queue.push(nums[i]);
            //由我们维护的单调队列的性质,只需取出队头的元素,即是滑动窗口中的最大值
            res.emplace_back(queue.getFront());
        }
        return res;
    }

整个解题代码如下所示

class Solution {
public:
    class Queue{
    public:
        void push(const int& val){
            while(!queue_.empty() && val>queue_.back())
                queue_.pop_back();
            queue_.emplace_back(val);
        }
        void pop(const int& val){
            if(!queue_.empty() && val==queue_.front())
                queue_.pop_front();
        }
        int getFront() const {
            return queue_.front();
        }
    private:
        std::deque<int> queue_;
    };
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Queue queue;
        std::vector<int> res;
        for(int i=0;i<k;++i)
            queue.push(nums[i]);
        res.emplace_back(queue.getFront());
        for(int i=k;i<nums.size();++i){
            queue.pop(nums[i-k]);
            queue.push(nums[i]);
            res.emplace_back(queue.getFront());
        }
        return res;
    }
};


http://www.kler.cn/a/386153.html

相关文章:

  • 如何优化Kafka消费者的性能
  • see的本质是什么?
  • PVE纵览-安装系统卡“Loading Driver”的快速解决方案
  • 2024 年(第 7 届)“泰迪杯”数据分析技能赛B 题 特殊医学用途配方食品数据分析 完整代码 结果 可视化分享
  • FPGA学习(10)-数码管
  • 深入探索:Scrapy深度爬取策略与实践
  • 《手写Spring渐进式源码实践》实践笔记(第十七章 数据类型转换)
  • 布朗运动
  • 如何用全局事件总线实现组件间的通信
  • STM32标准库-待机模式
  • 数据集市是什么?有什么优势?
  • OpenGL学习笔记(三) 绘制图形
  • RabbitMQ 篇-深入了解 RabbitMQ 安装以及 SpringAMQP 的基础使用(声明队列和交换机、发送接收消息、配置 JSON 消息转化器)
  • 【Linux系列】利用 CURL 发送 POST 请求
  • Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口
  • 网络协议都有哪些?
  • 软件工程技术专业在物联网应用开发中的关键技术与挑战
  • XSS详解
  • 【Rust设计模式之Fold模式】
  • Java 中的 Arrays.sort () 方法:排序的利器
  • GOT-OCR:开源免费的OCR项目,多语言多模态识别,端到端识别新体验!不仅能识别文字,连数学公式、图表都不在话下!
  • 服装品牌零售业态融合中的创新发展:以开源 AI 智能名片 S2B2C 商城小程序为视角
  • unity中 骨骼、纹理和材质关系
  • 软件工程 软考
  • 在 Bash 中获取 Python 模块变量列
  • 2023上半年上午(1~75)