【leetcode_C++_栈与队列_day10】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]

附加要求:时间复杂度控制在O(n)

思路:

我们需要一个队列,这个队列随着窗口的移动队列也一进一出,每次移动之后队列告诉我们里面的最大值是什么。

队列里的元素一定要是排序的而且要最大值放在出队口。

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

这种维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有这样的单调队列,需要我们自己来组件单调队列。

不要以为实现的单调队列就是对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢?

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

push操作的规则:如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止。

front:保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

239.滑动窗口最大值-2

那么我们用什么数据结构来实现这个单调队列呢?

使用deque最为合适,在文章栈与队列:来看看栈和队列不为人知的一面 (opens new window)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。

知识补充:

deque函数:

deque容器为一个给定*类型*的元素进行线性处理,像向量一样,它*能够快速地随机访问任一个元素*,并且能够高效地*插入和删除*容器的尾部元素。但它又与vector不同,*deque支持高效插入和删除容器的头部元素*,因此也叫做*双端队列*。deque类常用的函数如下。

(1) 构造函数

deque():创建一个空deque

deque(int nSize):创建一个deque,元素个数为nSize

deque(int nSize,const T& t):创建一个deque,元素个数为nSize,且值均为t

deque(const deque &):复制构造函数

(2) 增加函数

void push_front(const T& x):双端队列头部增加一个元素X

void *push_back(const T& x):双端队列尾部增加一个元素x*

iterator insert(iterator it,const T& x):双端队列中*某一元素前*增加一个元素x

void insert(iterator it,int n,const T& x):双端队列中*某一元素前*增加n个相同的元素x

void insert(iterator it,const_iterator first,const_iteratorlast):双端队列中*某一元素前*插入另一个相同类型向量的[forst,last)间的数据

(3) 删除函数

Iterator erase(iterator it):删除双端队列中的*某一个元素*

Iterator erase(iterator first,iterator last):删除双端队列中[first,last)中的元素

void pop_front():删除双端队列中*最前一个元素*

void pop_back():删除双端队列中*最后一个元素*

void clear():清空双端队列中最后一个元素

(4) 遍历函数

reference at(int pos):返回pos位置元素的引用

reference front():返回*首元素的引用*

reference back():返回*尾元素的引用*

iterator *begin*():返回向量头指针,指向第一个元素

iterator *end*():返回指向向量中最后一个元素下一个元素的指针(不包含在向量中)

reverse_iterator rbegin():反向迭代器,指向最后一个元素

reverse_iterator rend():反向迭代器,指向第一个元素的前一个元素

(5) 判断函数

bool empty() const:向量是否为空,若true,则向量中无元素

(6) 大小函数

Int size() const:返回向量中元素的个数

int max_size() const:返回最大可允许的双端对了元素数量值

(7) 其他函数

void swap(deque&):交换两个同类型向量的数据

void assign(int n,const T& x):向量中第n个元素的值设置为x

C++知识补充

img

关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。您也可以指定类的成员为 privateprotected

class Solution {
//此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
private:
    class MyQueue{
    public:
        deque<int> que;//使用deque来实现单调队列pop的功能实现:首先要判断是否为空,然后判断弹出的数值是不是队列出口处的数值,如果是,才能弹出
        void pop(int value){
            if(!que.empty()&&value==que.front())
            {
                que.pop_front();//pop_front():删除双端队列中最前一个元素
            }
        }
        //push功能是保持队列中从大到小的单调。如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素为止
        void push(int value){
            while(!que.empty()&&value>que.back()){
                que.pop_back();//pop_back():删除双端队列中最后一个元素
            }
            que.push_back(value);
        }
        // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
        int front(){
            return que.front();
        }
    };


public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> res;//用来保存结果
        MyQueue que;
        for(int i=0;i<k;i++)//先将前k个元素放进队列
        {
            que.push(nums[i]);
        }
        res.push_back(que.front());//res记录前k个元素中的最大值
        for(int i=k;i<nums.size();i++)
        {
            que.pop(nums[i-k]);//滑动窗口移除最前面的元素
            que.push(nums[i]);//滑动窗口前加入最后面的元素
            res.push_back(que.front());//记录对应的最大值
        }
        return res;
    }
};

347. 前 K 个高频元素

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

示例 1:

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

示例 2:

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

C++知识补充:

1.STL关联式容器

​ C++容器大致分为 2 类,即序列式容器和关联式容器。其中,序列式容器(包括 array、vector、list、deque 和 forward_list),那么关联器容器是什么?

​ 序列式容器,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string 等)或使用结构体自定义类型的元素。例如,如下是一个存储 int 类型元素的 vector 容器:

std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};

​ 关联器容器则不大一样,此类容器在存储元素值的同时,还会为各元素再额外配备一个值或者叫“键”。它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。

弃用序列式容器,转而选用关联式容器存储元素,往往就是看中了关联式容器可以快速查找、读取或者删除所存储的元素,同时该类型容器插入元素的效率也比序列式容器高。

除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。

C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset.

image-20221031163001572

2.STL pair用法详解

​ 我们知道,关联式容器存储的是“键值对”形式的数据.其中第一个元素作为键(key),第二个元素作为值(value).考虑到“键值对”并不是普通类型数据,C++ STL 标准库提供了 pair 类模板,其专门用来将 2 个普通元素 first 和 second(可以是 C++ 基本数据类型、结构体、类自定的类型)创建成一个新元素<first, second>。通过其构成的元素格式不难看出,使用 pair 类模板来创建“键值对”形式的元素,再合适不过。

3.priority_queue(STL priority_queue)用法详解

​ priority_queue 容器适配器定义了一个元素有序排列的队列。默认队列头部的元素优先级最高。因为它是一个队列,所以只能访问第一个元素,这也意味着优先级最高的元素总是第一个被处理。但是如何定义“优先级”完全取决于我们自己。如果一个优先级队列记录的是医院里等待接受急救的病人,那么病人病情的严重性就是优先级。如果队列元素是银行的借贷业务,那么借记可能会优先于信贷。

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

priority_queue<Type, Container, Functional>

Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。

例如:

template <typename T, typename Container=std::vector<T>, typename Compare=std::less<T>> class priority_queue

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

​ greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

4.operator()关键字

c++ 中的operator()有两大主要作用:

  1. Overloading------重载()操作符;
  2. Casting------实现对象类型转化。

​ 一. 重载()操作符

函数对象:定义了调用操作符()的类对象。当用该对象调用()操作符时,其表现形式如同普通函数一致,因此取名为函数对象。与普通函数相比,函数对象更加灵活,代码看上去更加优雅。

  • 函数对象有自己的状态。可以在类中定义状态变量,这样一个函数对象在多次的调用中可以共享这个状态;
  • 函数对象有自己特有的类型。可以传递相应的类型作为参数来实例化相应的模块,比如带参数的函数形参。

函数对象举例:

class A
{
public:
  mutable int var;
  int operator() (int value) //重载()运算符,传入int的参数,operator()可以传入无限制的参数
  {
    return value > var ? value : var-value;
  }
};

int main()
{
  int i = -1;
  A func;
  std::cout << func(i) << std::endl; //实际上调用的是func.operator()(i)这个函数
  return 0;
}

​ 二.类型转换

在c++中可以用operator Type()的形式定义类型转换函数,将类对象转换为Type类型

class A
{
public:
  mutable int var;
  void setVar(int a)
  {
    var = a;
  }

  operator int()//将类A对象隐式转化为int类型
  {
    return var;
  }
};

int main()
{
  A func;
  func.setVar(10);
  std::cout << func << std::endl;//实际上调用的是func.operator int()这个函数
  return 0;
}
output
10

4.iterator 迭代器

迭代器(iterator)是一中检查容器内元素并遍历元素的数据类型。

(1) 每种容器类型都定义了自己的迭代器类型,如vector:
vector::iterator iter;这条语句定义了一个名为iter的变量,它的数据类型是由vector定义的iterator类型。

(2) 使用迭代器读取vector中的每一个元素:

vector<int>::iterator

vector<int> ivec(10,1);
for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
{
	*iter=2; //使用 * 访问迭代器所指向的元素
}

const_iterator
只能读取容器中的元素,而不能修改。

for(vector<int>::const_iterator citer=ivec.begin();citer!=ivec.end();citer++)
{
	cout<<*citer;
//*citer=3; error
}

vector::const_iterator 和 const vector::iterator的区别
const vector::iterator newiter=ivec.begin();
*newiter=11; //可以修改指向容器的元素
//newiter++; //迭代器本身不能被修改

(3) iterator的算术操作:
iterator除了进行++,--操作,可以将iter+n,iter-n赋给一个新的iteraor对象。还可以使用一个iterator减去另外一个iterator.

const vector<int>::iterator newiter=ivec.begin();
vector<int>::iterator newiter2=ivec.end();
cout<<"\n"<<newiter2-newiter;

思路:

  1. 要统计元素出现频率

  2. 对频率排序

  3. 找出前K个高频元素

  4. 统计元素出现的频率

    使用map:元素值放在key中,权重放在value中。

  5. 对频率排序。

    使用容器适配器优先级队列priority_queue。priority_queue可以分为大顶堆和小顶堆对元素进行排序。从小到大排就是小顶堆,从大到小排就是大顶堆。

    为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

  6. 找出前K个高频元素

​ 如果使用大顶堆,每次更新弹出的都是最大值,不利于统计前k个元素。只有小顶堆每次将最小的元素弹出, 最后小顶堆里累计的才是前K个最大的元素。不过需要注意的是,按小顶堆来的存储结果需要倒序输出。

347.前K个高频元素

class Solution {
public:
    //小顶堆
    class mycomparison{
    public:
    //pair:每行都表示一个键值对,其中第一个元素作为键(key),第二个元素作为值(value)。
    //在这里,key是元素的值,value是该元素的权重
    //C++ STL 标准库提供了 pair 类模板,其专门用来将 2 个普通元素 first 和 second
        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);//把扫描到的数值push进顶堆
            if(pri_que.size()>k)//如果堆的大小大于了k,则队列弹出,保证堆的大小一直为K
            {
                pri_que.pop();
            }
            //vector<int>::iterator iter;这条语句定义了一个名为iter的变量,它的数据类型是由vector<int>定义的iterator类型。
        }

        //找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for(int i=k-1;i>=0;i--)
        {
            result[i]=pri_que.top().first;//first里存放是key
            pri_que.pop();
        }
        return result;
    }
};
posted @ 2022-10-31 21:12  只想毕业的菜狗  阅读(53)  评论(0)    收藏  举报