题目:239. 滑动窗口最大值、347.前 K 个高频元素
参考链接:代码随想录
基础知识
cpp中的优先队列,需要#include <queue>
,本质是堆的实现,分为大顶堆和小顶堆。内置函数和queue类似,包括top,empty,size,push,emplace,pop,swap。
priority_queue<Type, Container, Functional>//定义,其中container是容器类型(vector或deque)
// Functional是比较方式
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
建立的堆默认为大顶堆,即最大优先队列,每次出队的时候都会先出最大元素。容器类型默认为vector。
cpp中还有双向队列deque。可以向两端发展。接口如下:
deq.size();
deq.max_size();
deq.resize();
deq.empty();
deq.push_front(const T& x);
deq.push_back(const T& x);
deq.pop_front();
deq.pop_back();
deq.clear();
deq.front();
deq.back();
//还有许多复杂度O(n)的接口
239. 滑动窗口最大值
思路:本题首先想到的是暴力算法,时间复杂度O(n*k)。由于需要返回滑动窗口最大值,可以想到使用优先队列的结构。由于需要计算最大值,故将窗口中的元素放在一个大顶堆中,当窗口滑动时,逐渐计算最大值,但这样的问题是无法在窗口移动的同时将出窗口的元素剔除出去。
看了解析,需要用到一种叫做单调队列的结构。即队列里面始终单调递增或者单调递减。本题选择单调递减,即front()出口始终为窗口中最大元素。队列中不需要维护整个窗口,只需要维护可能成为最大值的元素。push()操作即当一个元素从back入队时,如果其大于目前队尾,则其可能成为最大值,此时将队尾出队,不停弹出,直到其小于等于队尾,此时满足单调递减队列。对pop(),如果目前窗口移除的元素等于top(),此时队首需要出队,其他时候不用操作。时间复杂度O(n)。
class Solution {
public:
class MyQueue{
public:
deque<int> deq;
void push(int x){
if(deq.empty()||x<=deq.back()){//一定要确保队列单调递减
deq.push_back(x);
}
else{
while(!deq.empty()&&x>deq.back()){
deq.pop_back();
}
deq.push_back(x);
}
}
void pop(int x){//每次pop都是一个元素,代表窗口滑动
if(!deq.empty()&&x==deq.front()){
deq.pop_front();
}
}
int front(){
return deq.front();
}
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue pq;
vector<int> ans;
int i;
for(i=0;i<k;i++){
pq.push(nums[i]);
// cout << "hello" << endl;
}
ans.push_back(pq.front());
for(;i<nums.size();i++){
pq.pop(nums[i-k]);
pq.push(nums[i]);
ans.push_back(pq.front());
}
return ans;
}
};
写的时候一开始出了点错,首先是push的时候是和队尾作比较以保证单调递减,而不是队头。然后是while循环中必须确保队列不为空才可能比较front(),否则报错。标答的push操作比我更简单一点,只用一个while就搞定了。
标答:
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时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();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是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()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
347.前 K 个高频元素
思路:我一开始想的是使用一个unordered_map来记录每个元素的频次,然后使用大顶堆,将前k个频次记录下来,但是在寻找具体元素的时候遇到了麻烦,对每个元素都需要在map中遍历一次,时间复杂度较高。看了解析,发现需要使用小顶堆,因为大顶堆的实际上是需要将所有元素做一个排序,而小顶堆只要每次弹出最小元素,最后留下的k个元素就是最大的。而且小顶堆中需要存放一对pair<int,int>,方便找元素。时间复杂度O(nlogk)。
class Solution {
public:
struct compare{//仿函数,这里写结构体和类都可以
bool operator()(const pair<int,int> &p1,const pair<int,int> &p2){
return p1.second>p2.second;//记住是左大于右是小顶堆,相当于greater
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> mp;
for(int i:nums){//统计频率
mp[i]++;
}
priority_queue<pair<int,int>,vector<pair<int,int>>,compare > pq;//小顶堆
for(auto it=mp.begin();it!=mp.end();it++){
pq.push(*it);
if(pq.size()>k){//如果队列长度大于k,则出队最小元素,最后剩下的一定是k个最大
pq.pop();
}
}
vector<int> ans;//record用于记录k个最大频率
while(!pq.empty()){
ans.push_back(pq.top().first);
pq.pop();
}
return ans;
}
};
写的时候注意仿函数的写法。