代码算法训练营day13 | 239. 滑动窗口最大值、347.前 K 个高频元素

239. 滑动窗口最大值

题目链接
状态:暴力ok
文档:programmercarl.com

1、(暴力算法)思路:

一共有两层循环,外层循环(N)迭代变量,内层循环(K)每一个窗口,并计算这个窗口的最大值。时间复杂度O(N*K)。

代码:

class Solution {
public:
//暴力
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {  
        //短路条件
        int size = nums.size();
        //存放结果的数组
        vector<int> result;
        if(size == 0) return result;

        for(int i=0;i<=size-k;i++)
        {
            //最大值
            int max = nums[i];
            for(int j=i;j<=i+k-1;j++)
            {
                //计算每个窗口内的最大值
                if(nums[j] > max) max = nums[j];
            }
            result.push_back(max);
        }
        return result;
    }
};

2、(单调队列)思路:

239思路图
可以看到,每移动一次窗口,原理上都是把左边的元素pop出去,右边新加一个元素进来。
但是有时候并不需要去pop那个被窗口抛弃的元素,因为在某些元素push进来之前,我们会先对队列进行一个判断大小的操作:

  • 如果要push进来的元素比队列入口处的元素大,那么就要把这些元素一 一pop掉,(因此之后可能就不用去pop那个被窗口抛弃的元素,因为已经被pop掉了)这样就保证了队列是一个单调递减的队列。
  • 之后若要pop出口处的元素,那么下一个元素一定是下一个窗口的最大值。
  • 并且,若pop的元素==队列出口的元素,那么说明pop的元素是这个窗口的最大值。

代码:

class Solution {
private:
    class MyQueue{
    public:
        //定义一个队列que,实现单调队列
        deque<int> que;
        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
        // 如果不相等的话说明此时要pop的数比出口数小,在出口数push前该数就已经被pop了。
        // 同时pop之前判断队列当前是否为空。
        void pop(int value){
            if(!que.empty() && value == que.front())
                que.pop_front();
        }

        // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
        // 这样就保持了队列里的数值是单调从大到小的了。
        void push(int value){
            while(!que.empty() && value > que.back())
                que.pop_back(); // 把小的数都pop
            //小的数pop完再push新的大的数
            que.push_back(value);
        }
        
        // 返回窗口最大值
        int getWindowMax(){
            return que.front();
        }
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;
        vector<int> result;
        //先把前k个数放到que中
        for(int i=0;i<k;i++){
            que.push(nums[i]);  // 已经保证现在的队列是一个单调队列了
        }
        // 此时已经产生了一个窗口的最大值,记录下来
        result.push_back(que.getWindowMax()); 

        // 窗口移动时,左边的元素被移除,右边的元素会新加进来
        for(int j=k;j<nums.size();j++){
            // 左边的元素移除掉
            que.pop(nums[j-k]);
            // 右边的元素加进来
            que.push(nums[j]);
            // 形成了新的窗口,返回窗口的最大值
            result.push_back(que.getWindowMax()); 
        }
        return result;
    }
};

注意:
在push函数中,判断要加进来的value是否>队列入口处的值时,因为这个比较次数是不确定的,所以不能用if判断,而要用while判断才行。


347.前 K 个高频元素

思路:

要做的事情一共有三个:

  1. 要统计元素出现频率(这一类的问题可以使用map来进行统计。key:存放元素,value:元素出现的次数)
  2. 对频率排序(这里我们可以使用一种 容器适配器就是优先级队列。)
  3. 找出前K个高频元素

如果在对排序的时候不使用优先级队列,而是直接使用map呢? ----- 在map中以value为基准,进行一个从大到小的排序,然后把满足条件的value所对应的key输出就可以了。
但是按照快排来看,对所有的元素都进行一个排序,时间复杂度是nlogn。
但其实并没有必要对所有的元素进行一个排序,只需要对前k个集合的元素进行排序即可。
如何去维护k个高频元素的有序集合呢? ---- 大顶堆和小顶堆
大顶堆和小顶堆本质上是一个二叉树。

优先级队列是什么?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

堆是什么?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

大顶堆:根节点最大,父亲比左右孩子要大。
小顶堆:根节点最小,父亲比左右孩子要小。

选哪个堆?

用堆去遍历map中的元素,因为我要输出前k个高频元素,所以堆的大小就设置成k,也就是说堆里面就维持k个元素。遍历完之后,这个堆就维护了我们想要求的前k个高频元素。

  • 如果要使用大顶堆的话:堆里目前已经满足k个元素了,此时如果push进一个新的元素,那么就意味着有一个元素要被pop掉了。对于这种数据结构,要pop的时候都是从栈顶开始pop的。大顶堆的栈顶是最大的数,那就意味着把最大的数给pop掉了,这肯定是不合理的。
  • 反之如果使用小顶堆的话,pop掉的栈顶元素是最小的值,这样一次次的就把最小的值弄出去,留下的就是大的值。所以此题选用小顶堆。

用小顶堆的时间复杂度:把map中的元素遍历一遍(n),然后对二叉树进行调整(push+pop,logk)。所以小顶堆的时间复杂度是nlogk。

347思路图
代码:

class Solution {
public:
    //先构建一个小顶堆
    class mycomparison{
    public:
        //要对键值对(<key,value>)进行排序,还要控制对谁(value)排序
        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
        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();
            }
        }

         // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
         vector<int> result(k);
         for(int i = k-1;i>=0;i--){
            result[i] = pri_que.top().first;
            // 弹出栈顶元素 以便下一次继续输出top
            pri_que.pop();
         }

         return result;
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值