day 13 第五章 栈与队列
今日内容:
● 239. 滑动窗口最大值
● 347.前 K 个高频元素
● 总结
详细布置
239. 滑动窗口最大值 (一刷至少需要理解思路)
【链接】(文章,视频,题目)
之前讲的都是栈的应用,这次该是队列的应用了。
本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
-
暴力,两层for,显然时间复杂度是O(n * k) 。超出时间限制。
-
提升效率的关键是,如何在滑动窗口移动一位的时候,使用前一个的大部分共同元素信息。可以维护一个单调队列(有序队列)来保存滑动窗口内的元素,这样求滑动窗口内最大值,就只是O(1)操作。但是滑动窗口(单调队列)的维护:
-
出队,找到值为删除的元素,并删除(一半元素转圈平移)
-
入队,找到值为插入的元素,并插入(一半元素转圈平移)
-
看起来都是O(k/2)时间复杂度,整体还是O(n * k)
-
感觉维护一个环形队列会更好?
-
【看后想法】
-
需要每次获取滑动窗口中的最大值,不需要下标,考虑单调队列。由于每次新增和移除中已有一个元素,不关注整个窗口中的全部k个元素。为了降低新增删除的时间复杂度,只把有可能成为“窗口中最大值”的元素存储在单调队列中,而不是全部k个。有可能成为“窗口中最大值”的元素,满足两个条件:“足够大”是窗口中最大的,“足够新”,必须在滑动窗口中
-
单调队列头部(出口)。肯定满足足够大,因为最大元素必然在单调队列头部。要满足足够新,则要关注这个元素何时移除出滑动窗口,需要同时执行队列头部移除操作。
-
单调队列尾部(入口),是候选者的守门员。足够新。每次新进窗口的元素,也即需要新压入的元素,都在挑战原有尾部入口元素。新压入元素一定是新的:队尾入口(旧)元素在滑动窗口存在期间,新压入元素必然也存在。如果新压入元素比队尾入口元素还要大,那么则新元素“又大又新”,队尾入口元素“永无出头之日”,不可能成为滑动窗口最大值,因此可以直接把队尾元素移除。这是一个while条件,直至新压入元素<=队尾入口元素。新压入元素新而不大,原队尾入口元素大而不新,都有可能成为队列头
-
上述操作,是为了保证队列的单调性,头大尾小
-
上述定义的单调队列,可以称之为,找系列元素最值的专用数据结构,在这个系列元素本身新增删除的时候,仍然可以以O(1)的时间找到最值。本题中的系列元素就是滑动窗口的元素。其新增删除对应滑动窗口的移动
-
整个理解过程,需要辅助代码随想录的动图和拆解每一步的静态图片。TODO
-
【实现困难】
- 要在队列尾部移除元素,用双端队列deque
【自写代码】
class Solution {
public:
class MyQueue {
std::deque<int> queue_;
public:
MyQueue(){}
// 弹出一个值,仅当在队列头才弹出
void pop(int val) {
if (!queue_.empty() && queue_.front() == val) {
queue_.pop_front();
}
}
// 压入一个值,与队列尾作比较
void push(int val) {
while (!queue_.empty() && val > queue_.back()) {
queue_.pop_back();
}
queue_.push_back(val);
}
int front() {
return queue_.front();
}
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 使用单调队列存储可能成为滑动窗口最大值的候选,双端队列deque实现
vector<int> res;
MyQueue que;
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
res.emplace_back(que.front());
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]);
que.push(nums[i]);
res.emplace_back(que.front());
}
return res;
}
};
【收获与时长】2h
347.前 K 个高频元素 (一刷至少需要理解思路)
【链接】(文章,视频,题目)
大/小顶堆的应用, 在C++中就是优先级队列
本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
-
map<int, int>,值,出现次数
-
对频率进行排序,没想好,尝试用sort函数加上lambda表达式
【看后想法】
-
本质上要做三件事
-
对值出现的次数做统计,无序映射:值,出现次数
-
根据出现次数做排序,
-
选取前k大的值
-
-
排序选前k大的,使用小顶堆,把小的堆顶移除,剩余k个就是前k大的
-
优先级队列的反直觉的定义:当运算符为less,表示大顶堆。记忆上,方便自己理解:
-
优先级队列尾部,1<2<3<4,头部
【实现困难】
-
自定义仿函数(函数对象),其实是一个排序类,注意细节
-
class结尾分号;
-
public:
-
bool operator() (const type& lhs, const type& rhs) {…}
-
【自写代码】
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// 使用优先级队列
class mygreator{
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
// 统计每个值的出现次数
std::unordered_map<int, int> umap; // 值,出现次数
for (const auto& num : nums) {
umap[num]++;
}
// 实现小顶堆,找前k大
std::priority_queue<pair<int, int>, vector<pair<int, int>>, mygreator> pq;
for (auto iter = umap.begin(); iter != umap.end(); iter++) {
pq.push(*iter);
if (pq.size() > k) {
pq.pop();
}
}
vector<int> res;
while (!pq.empty()) {
res.emplace_back(pq.top().first);
pq.pop();
}
return res;
}
};
【收获与时长】
- 2h25m