(第19天)【leetcode题解】239、滑动窗口最大值 347、前K个高频元素

239、滑动窗口最大值

题目描述

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

返回每次滑动窗口中的最大值 。

思路

  1. 选择数据结构:队列(实现单调队列)
  1. 队列应该具有的性质
  • 元素值递减(从队头到队尾)
  • 队列只需要存储可能成为滑动窗口中最大值的元素即可,而不需要维护滑动窗口中所有的值**(这规定了队列功能的实现方式)**
  1. 队列应该具有的功能:
  • pop()滑动窗口移动时移除元素
  • push()滑动窗口移动时添加的数值
  • front()返回最大值
  1. 算法
  1. 数据结构的实现算法
  • push():当滑动窗口新元素大于队尾元素时,把这个队尾元素弹出;当这个新元素小于队尾元素(或队列为空)时,把这个元素添加进队尾
  • pop():当滑动窗口移除的元素和队头元素相同时,再移除队头元素
  • front():返回队头元素(也就是当前滑动窗口的最大值)
  1. 解决算法:
  • 先将第一个滑动窗口中的元素加入单调队列中
  • 使用遍历的方式移动滑动窗口
  • 每到一个新的滑动窗口,在队列中移除因窗口移动而移除的值
  • 每到一个新的滑动窗口,在队列中添加因窗口移动而添加的新值
  • 每到一个新的滑动窗口,更新完单调队列后,将队头元素(就是当前滑动窗口中的最大值)添加进结果集
  • 返回结果集

代码

class Solution {
private:
    class MyQueue {//单调队列
        public:
            deque<int> que;//使用deque来实现单调队列

            //如果滑动窗口移动时要移除的元素等于队头元素时,则移除队头元素
            //value:滑动窗口移动时要移除的元素
            void pop(int value) {
                if (!que.empty() && value == que.front()) que.pop_front();//移除队头元素
            }

            //要保证队列从大到小
            //value:滑动窗口移动时要添加的元素值
            void push(int value) {
                //当队尾的值小于滑动窗口要添加的值时,直到队尾元素大于value或队列为空
                while (!que.empty() && value > que.back()) {
                    que.pop_back();//移除队尾元素
                }
                que.push_back(value);//把新元素添加进队尾
            }

            //返回队列最大值(队头)
            int front() {
                return que.front();
            }
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;//单调队列
        vector<int> res;//存储结果
        for (int i = 0; i < k; i++) {
            //先将前k个元素放入单调队列  也就是第一个滑动窗口
            que.push(nums[i]);
        }
        res.push_back(que.front());//记录第一个滑动窗口中的最大值

        //滑动窗口向前移动
        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]);//在队列中移除因滑动窗口移动而移除的元素
            que.push(nums[i]);//在队列中加入滑动窗口中的新元素
            res.push_back(que.front());//在结果集中添加新滑动窗口中的最大值
        }

        return res;
    }
};

时间复杂度:O(n);nums中的元素最多被push()和pop()队列一次,因此总体时间复杂度为O(n);
空间复杂度:O(k);队列中的元素最多不会超过k个元素。

对单调队列的思考

  • deque:双端队列。在C++中是queue的默认底层实现容器。在这里用deque作为底层容器实现单调队列。
  • 单调队列:从队头到队尾递增或递减的队列。
  • (不用排序维护队列时)单调队列的实现要依靠push()的具体操作实现:当要添加的元素大于队尾元素时,不断把队尾元素移除,直到要添加的元素小于队尾元素或队列为空。这样的push操作能够确保队列元素排列具有单调性。

347、前K个高频元素

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

思路

  1. 题目分析
  • 需要统计nums中各元素出现的频率
  • 因为需要返回频率前k高的元素,所以需要对频率排序
  • 找出前k个元素返回
  1. 数据结构选择
  • 统计频率:unordered_map<int, int>
  • 对频率排序:优先级队列(小顶堆)
  1. 算法
  • 先遍历nums统计出各数字的频率存储到map中
  • 对统计出的频率进行排序:扫描map中所有频率的数值,用大小为k的小顶堆存储结果,最终小顶堆中的k个元素就是频率最高的k个元素
  • 把小顶堆中的k个元素加入结果集返回

代码

class Solution {
public:
    //小顶堆
    class mycomparion {
    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;//map<nums[i], 频数>
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }

        //对频率排序
        //定义一个优先级队列(小顶堆),大小为K
        //pair<nums[i],频数>
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparion> 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();//从顶部弹出
            }
        }

        //将小顶堆中已经找出的频率前k高的元素加入结果集
        vector<int> res(k);//结果集的大小为k
        for (int i = k - 1; i >= 0; i--) {
            res[i] = pri_que.top().first;//first代表元素
            pri_que.pop();//把已经加入结果集的元素弹出
        }
        return res;
        }
};

时间复杂度:O(nlogk);先统计频数需要遍历nums花费O(n);之后遍历频率map最多花费O(n),其中每次堆操作至多花费O(logk);总体花费O(nlogk)。
空间复杂度:O(n);哈希表大小为O(n),堆大小为O(k)。

思考

  • map常用来统计数字出现的频率。
  • 除了常用的排序算法外,还可以使用优先级队列(堆)来堆数据排序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值