【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()就可以返回当前窗口的最大值。

那么我们用什么数据结构来实现这个单调队列呢?
使用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++知识补充

关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。您也可以指定类的成员为 private 或 protected。
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.

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
greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
4.operator()关键字
c++ 中的operator()有两大主要作用:
- Overloading------重载()操作符;
- 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
(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 vector
*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;
思路:
-
要统计元素出现频率
-
对频率排序
-
找出前K个高频元素
-
统计元素出现的频率
使用map:元素值放在key中,权重放在value中。
-
对频率排序。
使用容器适配器优先级队列priority_queue。priority_queue可以分为大顶堆和小顶堆对元素进行排序。从小到大排就是小顶堆,从大到小排就是大顶堆。
为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。
-
找出前K个高频元素
如果使用大顶堆,每次更新弹出的都是最大值,不利于统计前k个元素。只有小顶堆每次将最小的元素弹出, 最后小顶堆里累计的才是前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;
}
};
本文来自博客园,作者:只想毕业的菜狗,转载请注明原文链接:https://www.cnblogs.com/MLcaigou/p/16845845.html
浙公网安备 33010602011771号