第三周第一天| 力扣239、 滑动窗口最大值 力扣347、前k个高频元素

力扣239、 滑动窗口最大值

思路:
这道题目的思路如果用暴力法来求解的话,时间复杂度是O(n*k),因为相当于每滑一下,再遍历一遍,这样的话当k变的较大后效率是不够高的,本题没有指定k,那么有没有更好的方法来解决呢?

优先队列可不可以?如果使用优先队列,的确是能够在窗口中筛选出最大值,但是如何在窗口滑动后踢出窗口最前面的元素呢,毕竟窗口最前面的元素既不一定能保证在优先队列最前面,也不能保证在最后面,因此使用优先队列是不可取的,没法有效的移动窗口。

实际上,本题适合使用单调队列来求解,如果使用单调队列的话,那么能有什么好处呢?以单调递减为例,首先可以保证队头元素就是所需要获取的滑动窗口的最大值,因为单调递减嘛,其次,使用单调队列可以自定义插入和删除的方式,使得我有机会让无效的元素提前出局。
什么是无效的元素?如果在一个队列中,前面的元素都比我新加入的元素要来的小,那么它们在今后滑动窗口的过程中,还有机会成为最大值吗?显然是没有了,因为我新加入的元素就已经比他们要大了,所以这些元素此时就已经可以出局了,这是本题实现的关键。

接下来我们来看看要实现本题还需要哪些函数,何时弹出?当要滑动窗口时,如果将要滑动窗口的值等于队列的队头元素的话,弹出,否则不处理。此外,在加一个获取最大值的函数就可以了,实际上也就是return队首元素。

总结:这道题目的难点首先在于如何降低时间复杂度,使用单调队列,对每一个元素只有加入出局这一趟处理,因此整体的时间复杂度是O(N),空间复杂度的话,由于使用了单调队列来存储滑动窗口的最大值,因此空间复杂度为O(k).其次,要明确何时删除无效元素,这样,本题的代码就不难写出来了。

代码:

class Solution {
public:
    class MyQueue{
        public:
        deque<int>que;
        //m每次弹出的时候,比较要弹出元素的值是否等于出口元素的值,若相等则弹出,否则不用操作
        //同时判断队列是否为空,为什么?好看的衬衫
        void pop(int value){
            if(value==que.front()){
                que.pop_front();
            }
        }
        //如果push的元素大于que入口处的元素,将入口处的元素从后端pop掉,直到push的值小于等于入口元素。
        //这样就保持了队列中的元素是由大到小排列的了
        void push(int value){
            while(!que.empty()&&value>que.back()){
                que.pop_back();
            }
            que.push_back(value);
        }
        //查找当前单调队列里的最大值,并将该值返回
        //本题为什么使用单调队列,它和取前k个高频元素那道题的区别在哪里
        int front(){
            return que.front();
        }
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;
        vector<int> result;
        for(int i=0;i<k;i++){
            //先将前k个元素放入单调队列中
            que.push(nums[i]);
        }
        result.push_back(que.front());//将前k个元素的最大值放入到结果集中
        for(int i=k;i<nums.size();i++){
            //当滑动窗口时,若单调队列的队头与nums[]k-i]相等则弹出,否则不管
            que.pop(nums[i-k]);
            //向滑动窗口加入新元素
            que.push(nums[i]);
            //更新滑动窗口最大值的集合
            result.push_back(que.front());
        }
        return result;
    }
};

力扣347、前k个高频元素

思路:这道题目是一道经典题,并且跟上一题形成了良好的对比,本题是一道使用优先队列的经典题。

为什么要使用优先队列?首先,优先队列的实质就是按照权值大小进行排序的二叉树,那么这正是前k个高频元素所需要做的事情,只需要最后将结果输入数组并返回结果集即可。当然了,在使用优先队列之前,我们首先肯定需要统计各个元素的频率,使用map即可,因为既要统计元素,又要统计频率。既然要使用优先队列,我们就要问,使用大根堆,还是小根堆?**如果使用的是大根堆的话,那么我维持一个大小为k的大根堆,我还没遍历完我在map中所统计的频率,我可能就已经把其中较高频的元素给他弹出去了。**这是不可以的,因此我们应该用小根堆,这样当我们不断的弹出较小的元素之后,我们所剩下来的,自然就是前k个高频元素了。

总结:这道题目统计频率时使用了map,优先队列的大小为k,综合来看空间复杂度应为O(N),对于时间复杂度,统计频率的时间复杂度为O(n),而不断更新优先队列的过程涉及遍历map以及对小根堆中的元素进行处理,小根堆的本质是一棵有序的完全二叉树,处理其中元素的效率是O(logk),综合来看,时间复杂度应为O(Nlogk),比直接对map进行快排,然后输出结果的时间复杂度O(Nlogn)要好,虽然看上去差不多,但是在n>>k时,差距还是比较明显的。

代码:

class Solution {
public:
    class mycomparison{
        public:
        bool operator()(const pair<int,int> &lhs,const pair<int,int> &rhs){
            return lhs.second>rhs.second;
        }
    };
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        //要先统计各元素出现的频率,使用map
        unordered_map<int,int>map;//map<nums[i],对应元素出现的次数>
        for(int i=0;i<nums.size();i++){
            map[nums[i]]++;
        }
        
        //以各元素出现的频率为基准进行排序
        //定义一个小根堆,大小为k
        priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison>pri_que;
       
        //用固定大小为k的小根堆,循环遍历map,扫描所有频率的数值
        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();
            }
        }
        //找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
        vector<int>result(k);
        for(int i=k-1;i>=0;i--){
            result[i]=pri_que.top().first;
            pri_que.pop();
        }
        return result;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值