Day 13 239. 滑动窗口最大值 347.前 K 个高频元素 总结

滑动窗口最大值

给定一个数组 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]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

​ 直观想法是依次遍历每次滑动窗口里的k个元素在其中寻找最大值,然后依次返回滑动窗口中的最大值,时间复杂度显而易见是O(n*k),题目要求是用线性复杂度实现,所以不考虑暴力法;

​ 将滑动窗口视为一个队列,则需要做的是先将k个元素全部push进行,然后pop出队列尾部的元素(滑动窗口读取到的一个元素),push进滑动窗口移动后识别的元素,然后返回窗口内的最大值;即需要实现拥有这种功能的自定义数据结构,大概长这样:

class MyQueue {
public:
    void pop(int value) {
    }
    void push(int value) {
    }
    int front() {
        return que.front();
    }
};

​ 如果使用大顶堆(底层为完全二叉树)实现的话,虽然能够实现返回滑动窗口内的最大值,但是只能弹出最大值,所以无法确保滑动窗口正常移动

​ 所以需要的这种自定义数据结构,用单纯的队列或者大顶堆肯定不行,而是需要实现单调数列,这样才能确保每次返回的值都是最大值;但一旦对队列进行排序,如何确定保持滑动窗口移动;

​ 这时候就需要搞清楚题目的要求是什么了,得到滑动窗口中的最大值,那么并不需要维护整个滑动窗口;

​ 如何确保出口处是最大值:只要push进来的值大于前面的值,就把前面的值全部pop_back弹出,然后再push进队列;

​ 至于担心会不会直接把最大值pop出去:因为每次pop_front的时候都是处理完整个滑动窗口最大值,所以当pop出去最大值的时候,一定是滑动窗口已经离开了此时最大值的时候;

​ 具体代码实现如下:

class Solution {
    private:
        class MyQue{    
        public:
        deque<int> MyQue;
        void pop(int val){
        if(!MyQue.empty() && val == MyQue.front()){        
        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
        // 同时pop之前判断队列当前是否为空。
            MyQue.pop_front();
            }
        }
        void push(int val){
        // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
        // 这样就保持了队列里的数值是单调从大到小的了。
            while(!MyQue.empty() && val > MyQue.back()){
                MyQue.pop_back();
            }
            MyQue.push_back(val);
        }
        int GetMax(){
            return MyQue.front();
        }
    };
    public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQue mq;
        vector<int> res;
        for(int i = 0; i < k; i++){
            mq.push(nums[i]);
        }
        res.push_back(mq.GetMax());
        for(int j = k; j < nums.size(); j++){//此时这个j的初始化位置对应的nums[k]就是滑动窗口即将滑到的元素
            mq.push(nums[j]);//直接判断条件是否成立,然后push进滑动窗口
            mq.pop(nums[j - k]);//依次寻找之前出现过的最大值,从nums[0]开始pop
            res.push_back(mq.GetMax());
        }
        return res;
    }
};

​ 时间复杂度: O(n)

​ 空间复杂度: O(k)

​ C++中deque是stack和queue默认的底层实现容器,deque是可以两边扩展的,且deque里元素并不是严格的连续分布的。

前k个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

  • 输入: nums = [1,1,1,2,2,3], k = 2
  • 输出: [1,2]

示例 2:

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

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O ( n log ⁡ n ) O(n \log n) O(nlogn) , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

​ 很直观的想法,map存储元素和出现次数,对多有元素进行排序,然后map的key存放元素值,value存放出现频率;

​ 额,快排,时间复杂度O(n log n),寄!

​ 换个思路,题目仅要求给出前k高的元素,在面临这种频率类的题的时候,考虑大顶堆小顶堆,由于求的是前k高的元素,所以此处用小顶堆实现;此时的时间复杂度是O(n log k),在n很大k较小的时候,性能有明显提升;

​ 在C++中,可以用优先队列实现堆,代码如下:

class Solution {
public:
    class MyComparison{
    //重载运算符()实现大于操作达到升序排列的小顶堆
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> map;
        for(int i = 0; i < nums.size(); i++){
            map[nums[i]]++;//记录元素频率
        // map.insert(make_pair<int,int>(nums[i],1));
        // 对于两种map容器std::map、std::unordered_map:
        // insert(std::make_pair(key, value)) 和 emplace(std::make_pair(key, value))重复插入同一个key的操作,
        // 二者都不会替换原先的key对应的value值,只有索引[]操作会改变value
        //  所以我在Day7里的四数之和的问题里的注释有误!!!!
        }
        //定义优先队列实现小顶堆
        //此处下去看怎么实现的
        priority_queue<pair<int, int>, vector<pair<int, int>>, MyComparison> pri_que;
        
        // 用固定大小为k的小顶堆,扫描所有频率的数值
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }

        vector<int> res(k);
        for(int i = k - 1; i >= 0; i--){
            res[i] = pri_que.top().first;
            pri_que.pop();
        }
        return res;
    }
};

栈与队列总结篇

​ 在编译原理里,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构;

递归的实现也是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因;

使用抽象程度越高的语言,越容易忽视其底层实现

​ 同理,使用库函数、系统提供的数据结构的时候,也一定要去思考其底层实现是什么,只有做到真的学通了,才算学懂!

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值