算法练习第12天|● 239. 滑动窗口最大值● 347.前 K 个高频元素

239.滑动窗口的最大值

力扣原题

题目描述:

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

思路分析:

        尝试使用单调队列来记录当前窗口中的最大值。如下图所示,单调队列的入队规则是如果入队的元素前面有比自己小的数,这些数全部被剔除。注意这里说的是剔除,而不是简单的出队。因为如果是单向队列的出队的话,那么单向队列在执行步骤四时可能需要先把数字3出队后才能继续通过出队的方式将-1剔除。这就导致最大值3从队列中消失了。

  所以为了更加方面的对多余元素进行剔除,单调队列在用STL容器中的双端deque进行实现,因为deque支持在队列两端进行入队、出队操作。如下图所示。这样在步骤二中的1、步骤四中的-1直接通过pop_back()进行剔除。很方便,而且还能保留最大值。

         综上所述,单调队列的目的就是为了维护窗口内最大的两个数。其结构大致长这样:

class MyQueue {
public:
    void push(int val) {
    }

    void pop(int val) {
    }

    int front() {
        return que.front();
    }
};

其中 push(int val) 表示val入队,它的规则是:如果val前面的元素比val小,那么就将这些数通过pop_back弹出,直到遇到比val大的数停止或队列为空停止。如上述的步骤二和步骤四。

pop(int val) 表示val出队,这种情况发生在滑动窗口向右移动的时候。如下图所示:

 窗口滑动之后,3不在是窗口内的数,所以需要使用pop_front将单调队列中的3弹出。这里的pop_front也体现出了deque的灵活性。

如果不满足 滑动窗口移动前最左端的值 = 单调队列最左(前)端的值 这个条件,则表明刚才窗口内的最大值还在移动口的窗口内,具体情况如下图所示:

综上所述,该单调队列的代码实现如下: 

class myQueue{  //自定义单调队列。从大到小排列,左边出队,右边入队
    public:
        deque<int> que;   //双端队列
        void push(int val)
        {   //从队列右端输入数据。若输入的数据val比队列末尾的若干个数大,
            //则将这些数弹出.直到队列为空或val遇到比自身大的数
            while(!que.empty() && que.back() < val)
            {
                que.pop_back();   //循环弹出比val小的数
            }

            //val入队
            que.push_back(val);
        }

        void pop(int val)  
        {
            //比较滑动窗口左端要弹出数值val是否是位于单调
            //队列左端的最大值,如果是,则弹出;如果不是,表明最大值这个数位于新的窗口内。
            if(!que.empty() && val == que.front())
            {
                que.pop_front();
            }
        }

        int front()  //单调队列最左端的值就是当前窗口的最大值
        {
            return que.front();
        }
    };

有了这样的单调队列,下面给出该题的解题代码:

class Solution {
private:
    class myQueue{  //自定义单调队列。从大到小排列,左边出队,右边入队
    public:
        deque<int> que;
        void push(int val)
        {   //从队列右端输入数据。若输入的数据val比队列末尾的数大,则将该数弹出.直到队列为空或val遇到比自身大的数
            while(!que.empty() && que.back() < val)
            {
                que.pop_back();   //循环弹出比val小的数
            }

            //val入队
            que.push_back(val);
        }

        void pop(int val)  //比较滑动窗口左端要弹出数值val是否是位于单调队列左端的最大值
        {
            if(!que.empty() && val == que.front())
            {
                que.pop_front();
            }
        }

        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());  //记录当前窗口最大值

        for(int i = k; i<nums.size();i++)//然后从下标k开始进行窗口移动
        {
            //i-k和i的使用非常巧妙地模拟了窗口向右滑动的过程
            que.pop(nums[i-k]);  //窗口滑动先要执行是否pop出来最大值
            que.push(nums[i]);  //新元素入队,重新调整
            result.push_back(que.front()); // 记录新的最大值
        }

        return result;
    }
};

347.前 K 个高频元素

力扣链接

题目描述:

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

思路分析:

这道题目主要涉及到如下三块内容:

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

然后是对频率进行排序,这里我们可以使用一种 新的容器适配器就是优先级队列priority_queue。其详细用法可以看博主爱喝酸奶!的这篇博文:C++ 优先队列 priority_queue 使用篇

优先队列使用vector存储数据,使用堆算法将内部的元素是自动按照元素的权值排列,对外提供的数据接口有插入push(val)、删除堆顶元素pop()、返回堆顶元素top()。

在C++的STL容器中,priority_queue所在的头文件是<queue>。我们使用unordered_map来统计各个元素及其出现的频率。然后将该map中的每一条数据加载到优先级队列中,该队列会按照权值(频率)进行堆排序,注意,是按照权值排序。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

如果使用大顶堆,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?

所以我们要用大小为k的小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。然后就可以依次通过top()和pop()接口获得者前k个。

由于本题要排序的是map结构的数据,所以在创建优先级队列(最小堆)时,需要自定义排序类型的仿函数二元谓词来实现排序。仿函数是指重载了小括号()的一个,注意,一定是重载了小括号()。谓词指的是重载小括号时函数的返回值是bool类型。一元谓词指重载的小括号能接受一个参数,二元谓词指能接受两个参数。因为我们要通过比较两个权重来对优先级队列进行堆排序,所以使用二元谓词。其结构如下:

class myCompare{
    public:
       //const 保证原数据不被误改,&引用避免拷贝
    bool operator()(const pair<int,int> &left, const pair<int,int> &right)
    {
          //注意这里的大于号,小顶堆是>号,大顶堆是<号。second表示权值,按照权值排序
          return left.second > right.second;  
    }
};

题目完整代码如下:

class Solution {
public:

    class myCompare{
        public:
            //const 保证原数据不被误改,&引用避免拷贝
            bool operator()(const pair<int,int> &left, const pair<int,int> &right)
            {
                //注意这里的大于号,小顶堆是>号,大顶堆是<号。second表示权值,按照权值排序
                return left.second > right.second;  
            }
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> map;
        for(auto num: nums)
        {
            ++map[num];   //统计各数字出现的频次,key为原数字,value为频次
        }

        //构建priority_queue。队列的每一个元素都是组对pair;用于接收上面map中的每一对key-value
        //使用pair的vector存储这些pair;
        //排序方式为自定义的myCompare,实现最小堆排序
        priority_queue< pair<int,int>, vector<pair<int,int>>, myCompare>  pri_que;

        //将map中的key-value依次入队,pri_que会按照myCompair进行最小堆排序
        for(unordered_map<int,int>::iterator it = map.begin(); it != map.end(); it++)
        {
            pri_que.push(*it);
            //判断队列长度是否超过k,若超过k,将堆顶的key-value弹出
            if(pri_que.size() > k){
                pri_que.pop();
            }
        }

        //等所有的key-value均遍历后,优先级队列也维护完成。可以依次top()和pop()来获取前k个元素了
        vector<int> result;
        while(!pri_que.empty())
        {
            result.push_back(pri_que.top().first);  //注意pri_que.top()是键值对,需要在使用first才能得到元素
            pri_que.pop();
        }

        return result;

    }
};

因为题目没有要求按频次的具体大小顺序进行结果输出,所以上述代码是将频率较低的元素存在了result对应vector的前面。如果想将频率较高的元素存前面,则可以使用下面这段代码:

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

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值