力扣239、 滑动窗口最大值
思路:
这道题目的思路如果用暴力法来求解的话,时间复杂度是O(n*k),因为相当于每滑一下,再遍历一遍,这样的话当k变的较大后效率是不够高的,本题没有指定k,那么有没有更好的方法来解决呢?
优先队列可不可以?如果使用优先队列,的确是能够在窗口中筛选出最大值,但是如何在窗口滑动后踢出窗口最前面的元素呢,毕竟窗口最前面的元素既不一定能保证在优先队列最前面,也不能保证在最后面,因此使用优先队列是不可取的,没法有效的移动窗口。
实际上,本题适合使用单调队列来求解,如果使用单调队列的话,那么能有什么好处呢?以单调递减为例,首先可以保证队头元素就是所需要获取的滑动窗口的最大值,因为单调递减嘛,其次,使用单调队列可以自定义插入和删除的方式,使得我有机会让无效的元素提前出局。
什么是无效的元素?如果在一个队列中,前面的元素都比我新加入的元素要来的小,那么它们在今后滑动窗口的过程中,还有机会成为最大值吗?显然是没有了,因为我新加入的元素就已经比他们要大了,所以这些元素此时就已经可以出局了,这是本题实现的关键。
接下来我们来看看要实现本题还需要哪些函数,何时弹出?当要滑动窗口时,如果将要滑动窗口的值等于队列的队头元素的话,弹出,否则不处理。此外,在加一个获取最大值的函数就可以了,实际上也就是return队首元素。
总结:这道题目的难点首先在于如何降低时间复杂度,使用单调队列,对每一个元素只有加入出局这一趟处理,因此整体的时间复杂度是O(N),空间复杂度的话,由于使用了单调队列来存储滑动窗口的最大值,因此空间复杂度为O(k).其次,要明确何时删除无效元素,这样,本题的代码就不难写出来了。
代码:
class Solution {
public:
class MyQueue{
public:
deque<int>que;
//m每次弹出的时候,比较要弹出元素的值是否等于出口元素的值,若相等则弹出,否则不用操作
//同时判断队列是否为空,为什么?好看的衬衫
void pop(int value){
if(value==que.front()){
que.pop_front();
}
}
//如果push的元素大于que入口处的元素,将入口处的元素从后端pop掉,直到push的值小于等于入口元素。
//这样就保持了队列中的元素是由大到小排列的了
void push(int value){
while(!que.empty()&&value>que.back()){
que.pop_back();
}
que.push_back(value);
}
//查找当前单调队列里的最大值,并将该值返回
//本题为什么使用单调队列,它和取前k个高频元素那道题的区别在哪里
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());//将前k个元素的最大值放入到结果集中
for(int i=k;i<nums.size();i++){
//当滑动窗口时,若单调队列的队头与nums[]k-i]相等则弹出,否则不管
que.pop(nums[i-k]);
//向滑动窗口加入新元素
que.push(nums[i]);
//更新滑动窗口最大值的集合
result.push_back(que.front());
}
return result;
}
};
力扣347、前k个高频元素
思路:这道题目是一道经典题,并且跟上一题形成了良好的对比,本题是一道使用优先队列的经典题。
为什么要使用优先队列?首先,优先队列的实质就是按照权值大小进行排序的二叉树,那么这正是前k个高频元素所需要做的事情,只需要最后将结果输入数组并返回结果集即可。当然了,在使用优先队列之前,我们首先肯定需要统计各个元素的频率,使用map即可,因为既要统计元素,又要统计频率。既然要使用优先队列,我们就要问,使用大根堆,还是小根堆?**如果使用的是大根堆的话,那么我维持一个大小为k的大根堆,我还没遍历完我在map中所统计的频率,我可能就已经把其中较高频的元素给他弹出去了。**这是不可以的,因此我们应该用小根堆,这样当我们不断的弹出较小的元素之后,我们所剩下来的,自然就是前k个高频元素了。
总结:这道题目统计频率时使用了map,优先队列的大小为k,综合来看空间复杂度应为O(N),对于时间复杂度,统计频率的时间复杂度为O(n),而不断更新优先队列的过程涉及遍历map以及对小根堆中的元素进行处理,小根堆的本质是一棵有序的完全二叉树,处理其中元素的效率是O(logk),综合来看,时间复杂度应为O(Nlogk),比直接对map进行快排,然后输出结果的时间复杂度O(Nlogn)要好,虽然看上去差不多,但是在n>>k时,差距还是比较明显的。
代码:
class Solution {
public:
class mycomparison{
public:
bool operator()(const pair<int,int> &lhs,const pair<int,int> &rhs){
return lhs.second>rhs.second;
}
};
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
//要先统计各元素出现的频率,使用map
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的小根堆,循环遍历map,扫描所有频率的数值
for(unordered_map<int,int>::iterator it=map.begin();it!=map.end();it++){
pri_que.push(*it);
if(pri_que.size()>k){//若小根堆的大小超过了k,弹出频率最小的元素,保证堆的大小恒为k
pri_que.pop();
}
}
//找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
vector<int>result(k);
for(int i=k-1;i>=0;i--){
result[i]=pri_que.top().first;
pri_que.pop();
}
return result;
}
};