【C++代码】滑动窗口最大值,前 K 个高频元素--代码随想录

题目:滑动窗口最大值

  • 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值
题解
  • 暴力解决(超出时间限制)

    • class Solution {
      public:
          vector<int> maxSlidingWindow(vector<int>& nums, int k) {
              vector<int> res(nums.size()-k+1);
              int index=0;
              while(index<=nums.size()-k){
                  vector<int> temp(k);
                  for(int i=0;i<k;i++){
                      temp[i] = nums[index+i];
                  }
                  int max_one=temp[0];
                  for(int i=1;i<k;i++){
                      if(max_one<temp[i]){
                          max_one=temp[i];
                      }
                  }
                  res[index]=max_one;
                  index++;
              }
              return res;
          }
      };
      
  • 此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。

  • 但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。

  • 其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的

  • 那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列。设计单调队列的时候,pop,和push操作要保持如下规则:

    • pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作

    • push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

  • 保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下:

    • 使用deque最为合适,提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。

  • private:
        class myQueue{
        public:
            deque<int> que;
            void pop(int value){
                if(!que.empty()&&value==que.front()){
                    que.pop_front();
                }
            }
            void push(int 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++){
                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);空间复杂度: O(k)

  • 首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本题中的单调队列实现就是固定的写法哈。

  • deque 是 double-ended queue 的缩写,又称双端队列容器。deque 容器也擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。deque 容器也可以根据需要修改自身的容量和大小。和 vector 不同的是,deque 还擅长在序列头部添加或删除元素,所耗费的时间复杂度也为常数阶O(1)。并且更重要的一点是,deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间中。当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器。

  • 通过拷贝其他类型容器中指定区域内的元素(也可以是普通数组),可以创建一个新容器,例如:

    • //拷贝普通数组,创建deque容器
      int a[] = { 1,2,3,4,5 };
      std::deque<int>d(a, a + 5);
      //适用于所有类型的容器
      std::array<int, 5>arr{ 11,12,13,14,15 };
      std::deque<int>d(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
      
  • 基于 deque 双端队列的特点,该容器包含一些 array、vector 容器都没有的成员函数。和 vector 相比,额外增加了实现在容器头部添加和删除元素的成员函数,同时删除了 capacity()、reserve() 和 data() 成员函数。

    • 函数成员函数功能
      begin()返回指向容器中第一个元素的迭代器。
      end()返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
      size()返回实际元素个数。
      max_size()返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 2 32 − 1 2^{32}-1 2321
      resize()改变实际元素的个数。
      empty()判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
      shrink _to_fit()将内存减少到等于当前元素实际所使用的大小。
      at()使用经过边界检查的索引访问元素。
      front()返回第一个元素的引用。
      back()返回最后一个元素的引用。
      assign()用新元素替换原有内容。
      push_back()在序列的尾部添加一个元素。
      push_front()在序列的头部添加一个元素。
      pop_back()移除容器尾部的元素。
      pop_front()移除容器头部的元素。
      insert()在指定的位置插入一个或多个元素。
      erase()移除一个元素或一段元素。
      clear()移出所有的元素,容器大小变为 0。
      swap()交换两个容器的所有元素。
      emplace()在指定的位置直接生成一个元素。
      emplace_front()在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
      emplace_back()在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。
  • 和 array、vector 相同,C++11 标准库新增的 begin() 和 end() 这 2 个全局函数也适用于 deque 容器。这 2 个函数的操作对象既可以是容器,也可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

  • #include <iostream>
    #include <deque>
    using namespace std;
    int main()
    {
        //初始化一个空deque容量
        deque<int>d;
        //向d容器中的尾部依次添加 1,2,3
        d.push_back(1); //{1}
        d.push_back(2); //{1,2}
        d.push_back(3); //{1,2,3}
        //向d容器的头部添加 0 
        d.push_front(0); //{0,1,2,3}
        //调用 size() 成员函数输出该容器存储的字符个数。
        printf("元素个数为:%d\n", d.size());
        //使用迭代器遍历容器
        for (auto i = d.begin(); i < d.end(); i++) {
            cout << *i << " ";
        }
        cout << endl;
        return 0;
    }
    

题目:前 K 个高频元素

  • 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
题解
  • 这道题目主要涉及到如下三块内容:要统计元素出现频率,对频率排序,找出前K个高频元素;

  • 首先统计元素出现的频率,这一类的问题可以使用map来进行统计。然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

  • 缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

  • 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

  • 所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素

  • 寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

    • 在这里插入图片描述

    • 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> temp_map;
              for(auto i=nums.begin();i!=nums.end();i++){
                  temp_map[(*i)]++;
              }
              priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison> pri_que;
              // 用固定大小为k的小顶堆,扫面所有频率的数值
              for(unordered_map<int,int>::iterator it=temp_map.begin();it!=temp_map.end();it++){
                  pri_que.push(*it);
                  if(pri_que.size()>k){
                      pri_que.pop();
                  }
              }
              for(auto kv:temp_map){
                  cout<<kv.first<<kv.second<<endl;
              }
              // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
              vector<int> res(k);
              for(int i=0;i<k;i++){
                  res[k-i-1]=pri_que.top().first;
                  pri_que.pop();
              }
              return res;
          }
      };
      
  • 时间复杂度: O(nlogk);空间复杂度: O(n)

  • priority_queue既然是队列那么先要包含头文件#include <queue>, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队。优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的和队列基本操作相同:

    • top 访问队头元素;empty 队列是否为空;size 返回队列内元素个数;push 插入元素到队尾 (并排序);emplace 原地构造一个元素并插入队列;pop 弹出队头元素;swap 交换内容
  • priority_queue 容器适配器定义了一个元素有序排列的队列。默认队列头部的元素优先级最高。因为它是一个队列,所以只能访问第一个元素,这也意味着优先级最高的元素总是第一个被处理。但是如何定义“优先级”完全取决于我们自己。如果一个优先级队列记录的是医院里等待接受急救的病人,那么病人病情的严重性就是优先级。如果队列元素是银行的借贷业务,那么借记可能会优先于信贷。

  • priority_queue 模板有 3 个参数,其中两个有默认的参数;第一个参数是存储对象的类型,第二个参数是存储元素的底层容器,第三个参数是函数对象,它定义了一个用来决定元素顺序的断言。因此模板类型是:

    • template <typename T, typename Container=std::vector<T>, typename Compare=std::less<T>> class priority_queue
      
  • priority_queue 实例默认有一个 vector 容器。函数对象类型 less<T> 是一个默认的排序断言,定义在头文件 function 中,决定了容器中最大的元素会排在队列前面。fonction 中定义了 greater<T>,用来作为模板的最后一个参数对元素排序,最小元素会排在队列前面。当然,如果指定模板的最后一个参数,就必须提供另外的两个模板类型参数。

    • 在这里插入图片描述

    • 显示元素的方式反映了它们被检索的顺序。在 vector 中它们也可以不像这样排序。

  • priority_queue 操作,对 priority_queue 进行操作有一些限制:

    • push(const T& obj):将obj的副本放到容器的适当位置,这通常会包含一个排序操作。

    • push(T&& obj):将obj放到容器的适当位置,这通常会包含一个排序操作。

    • emplace(T constructor a rgs…):通过调用传入参数的构造函数,在序列的适当位置构造一个T对象。为了维持优先顺序,通常需要一个排序操作。

    • top():返回优先级队列中第一个元素的引用。

    • pop():移除第一个元素。

    • size():返回队列中元素的个数。

    • empty():如果队列为空的话,返回true。

    • swap(priority_queue& other):和参数的元素进行交换,所包含对象的类型必须相同。

  • priority_queue 也实现了赋值运算,可以将右操作数的元素赋给左操作数;同时也定义了拷贝和移动版的赋值运算符。需要注意的是,priority_queue 容器并没有定义比较运算符。因为需要保持元素的顺序,所以添加元素通常会很慢。代码示例:

// Exercising a priority queue container adapter
#include <iostream> // For standard streams
#include <queue> // For priority_queue<T>
#include <string> // For string class
using std::string;
// List contents of a priority queue
template<typename T>
void list_pq(std::priority_queue<T> pq, size_t count = 5)
{
    size_t n {count};
    while (!pq.empty())
    {
        std::cout << pq.top() << " ";
        pq.pop();
        if (--n) continue;
        std::cout << std::endl;
        n = count;
    }
    std::cout << std::endl;
}
int main()
{
    std::priority_queue<std::string> words;
    std::string word;
    std::cout << "Enter words separated by spaces, enter Ctrl+Z on a separate line to end:\n";
    while (true)
    {
        if ((std::cin >> word).eof())
            break;
        words.push(word);
    }
    std::cout << "You entered " << words.size() << " words:" << std::endl;
    list_pq(words);
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羞儿

写作是兴趣,打赏看心情

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值