参考:
1.代码随想录
2.labuladong 的算法小抄 > 第一章、手把手刷数据结构 > 手把手设计数据结构 > 单调队列结构解决滑动窗口问题
3.【栈与队列】239滑动窗口最大值、347前 K 个高频元素
4.剑指 Offer 59 - II. 队列的最大值(单调双向队列,清晰图解)
5.如何解决 O(1) 复杂度的 API 设计题
6.347. 前 K 个高频元素
7.【C++】小白友好【优先队列的基础知识】
题目列表:
150. 逆波兰表达式求值【中等】
面试题59 - II. 队列的最大值【中等】
239. 滑动窗口最大值【困难】
剑指 Offer 59 - I. 滑动窗口的最大值【与239相同】
150. 逆波兰表达式求值【中等】
0.思路
逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
其实逆波兰表达式相当于是二叉树中的后序遍历。 可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。
- 逆波兰表达式主要有以下两个优点:
1.去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
2.适合用栈操作运算:
遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
1.方法
遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
2.理解
注意点:
-
力扣修改了后台测试数据,需要用longlong
stack<long long> temp;
-
出现运算符的时候的处理:
将字符串转换为整数stol , stoll 【参考】
取栈顶元素为num1,踢出,再取栈顶元素为num2,踢出,压入栈的是运算之后的结果
注意: 是num2 - / num1,for(auto c: tokens){ if(c=="+" || c=="-" || c=="*" ||c=="/"){ long long num1 = temp.top(); temp.pop(); long long num2 = temp.top(); temp.pop(); if(c== "+") temp.push(num2+num1); if(c== "-") temp.push(num2-num1); if(c== "*") temp.push(num2*num1); if(c== "/") temp.push(num2/num1); // 注意: 是num2 - / num1 }else{ temp.push(stoll(c)); } } int res = temp.top(); temp.pop(); return res;
3.代码
参考 代码随想录
239. 滑动窗口最大值【困难】
0.思路
- 先从整体来看,我们要先定位到第一个窗口,计算最大值,然后窗口不断向右移动,每次产生一个最大值。
同时,把这些最大值加入结果集,最后返回。 - 这里是分两阶段来进行的:
定位第一个窗口时,指针的起点是0,终点是k。
定位后续窗口时,指针的起点从k开始,每次加1,一直遍历到n-k。由于range是左闭右开,所以范围是(k, n-k+1)。
大的框架有了,我们再来看如何求每个窗口内的最大值。
1.方法
-
方法1:
对一个窗口,遍历其中所有元素求最大值。毫无疑问,时间复杂度是O(k)。整个算法的时间复杂度是O(nk)。 -
方法2:
维护一个单调队列,该队列中所有元素的值都是递减的,每次我们只需要取队头的元素,就知道当前的最大值。
首先,对于第一个窗口里的k个数,我们可以创建出一个单调队列。那么问题来了,每次窗口向右滑动的时候,我们怎么更新这个队列?
其实,移动窗口带来的变化,就是左边出去了一个元素,右边进来了一个元素。所以,维护单调队列的实质就是,我们怎么处理这两个元素。
(1)如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作。void pop(int value){ if(!que.empty() && value == que.front()){ que.pop_front(); } }
(2)如果进来的元素比队尾元素大,那么一直弹出队尾元素,直到不满足条件时,放入新元素。当然如果要进来的元素比队尾元素小,那么直接放入就可以。
void push(int value) { while (!que.empty() && value > que.back()) { que.pop_back(); } que.push_back(value); }
(3)操作完以上两步后,我们再取队头的元素,就是当前窗口的最大值。
所以,我们的单调队列要包含三个方法:push(放入新元素),pop(弹出队头),top(取队头但是不弹出)
最后,我们回过头来,其实在处理第一个窗口的时候,也可以用这样的思路,只不过变成了每个元素都是新进来的,没有出去的元素。
2. 理解
代码随想录里的图片,”单调队列如何维护队列里的元素“示意图。
过程理解如下:
- 首先,2进入队列(队尾为2),当3进入时候,根据方法2(2)中想法,“ 进来的元素比队尾元素大,那么一直弹出队尾元素”(则,弹出队尾2,新的队尾为3),5同理。
- 此时,5为队尾,1出现,“进来的元素比队尾元素小,那么直接放入就可以”,1直接进入。此时队尾为1,队列为 5,1
- 4再进入, “进来的元素比队尾元素大,那么一直弹出队尾元素”(则,弹出队尾1,新的队尾为4)
代码随想录里的图片,”窗口移动“示意图。
窗口大小为3,前3个“1, 3, -1 ” 的过程如上文,不赘述。
理解代码:
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]);
}
// 此时,队列里面的值,只有 3,-1。
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
// que.pop(nums[0]) num[0]= 1
// “如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作” 出去元素为1,对头元素为3,无操作
// 此时为 3,-1
que.push(nums[i]); // 滑动窗口前加入最后面的元素
// que.push(nums[3]); k=3,num[3]= -3
// -3进入,“ 要进来的元素比队尾元素小,那么直接放入”
// 此时为 3,-1,-3
result.push_back(que.front()); // 记录对应的最大值
// que.pop(nums[1]) num[1]= 3
// “如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作” 出去元素为3,对头元素为3,直接弹出
// 此时为 -1,-3
// que.push(nums[4]); k=3,num[4]= 5
// 5进入,“ 如果进来的元素比队尾元素大,那么一直弹出队尾元素” 弹出-3,再-1
// 此时为 5
}
return result;
}
验证:
3. 代码
参考 代码随想录
4. 注意点
单调队列的定义,分清pop 和push的作用,因为push里面也有弹出
-
void pop(int value)
什么时候需要使用que.pop( 滑动窗口的最左边 ) ?
是当滑动窗口移动的时候,需要移除最前面元素,判断移除的是否就是最大值。
是 if,就判断一次 -
void push(int value)
作用类似于整理当前滑动窗口里的元素。
是while,因为是不断整理
面试题59 - II. 队列的最大值【中等】是该题的基础版本。
解题方法:
参考 剑指 Offer 59 - II. 队列的最大值(单调双向队列,清晰图解)
参考 如何解决 O(1) 复杂度的 API 设计题
0. 题目:
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
1. 思路:
对于普通队列,入队push_back() 和出队pop_front() 的时间复杂度都是O(1)
本题难点为实现查找最大值 max_value()的O(1) 时间复杂度。
假设队列中存储 N 个元素,从中获取最大值需要遍历队列,时间复杂度为 O(N) ,单从算法上无优化空间。
2. 方法
最直观的想法是 维护一个最大值变量 ,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值 ,因此不可行。
考虑利用 数据结构 来实现,即经常使用的 “空间换时间” 。如下图所示,考虑构建一个递减列表来保存队列 所有递减的元素 ,递减链表随着入队和出队操作实时更新,这样队列最大元素就始终对应递减列表的首元素,实现了获取最大值 O(1) 时间复杂度。
知识点:C++中queue和deque的区别
queue从队首弹出,从队尾进入,先入先出,但是两端都能访问
deque可以访问两端并且可以在队首和队尾删除和插入元素
为了实现此递减列表,需要使用 双向队列 ,假设队列已经有若干元素:
- 当执行入队 push_back() 时: 若入队一个比队列某些元素更大的数字 x ,则为了保持此列表递减,需要将双向队列 尾部所有小于 x的元素 弹出。
- 当执行出队 pop_front() 时: 若出队的元素是最大元素,则 双向队列 需要同时 将首元素出队,以保持队列和双向队列的元素一致性。
使用双向队列原因:
维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。
简言之,单项维持列表的原顺序,双列表在于保证最大值,而且双列表可以从队尾弹出。
3. 理解
4. 代码
作者:jyd
链接
来源:力扣(LeetCode)
class MaxQueue {
queue<int> que;
deque<int> deq;
public:
MaxQueue() { }
int max_value() {
return deq.empty() ? -1 : deq.front();
}
void push_back(int value) {
que.push(value);
while(!deq.empty() && deq.back() < value)
deq.pop_back();
deq.push_back(value);
}
int pop_front() {
if(que.empty()) return -1;
int val = que.front();
if(val == deq.front())
deq.pop_front();
que.pop();
return val;
}
};
347. 前 K 个高频元素
0. 基础知识
优先队列
优先队列的底层是最大堆或最小堆。
-
定义
priority_queue <Type, Container, Functional>; static bool cmp(pair<int, int>& m, pair<int, int>& n) { return m.second > n.second; } priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
Type是要存放的数据类型
Container是实现底层堆的容器,必须是数组实现的容器,如vector、deque
Functional是比较方式/比较函数/优先级priority_queue<Type>;
此时默认的容器是vector,默认的比较方式是大顶堆less
//举例:
//小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//大顶堆
priority_queue <int,vector<int>,less<int> >q;
//默认大顶堆
priority_queue<int> a;
//pair
priority_queue<pair<int, int> > a;
pair<int, int> b(1, 2);
pair<int, int> c(1, 3);
pair<int, int> d(2, 5);
a.push(d);
a.push(c);
a.push(b);
while (!a.empty())
{
cout << a.top().first << ' ' << a.top().second << '\n';
a.pop();
}
//输出结果为:
2 5
1 3
1 2
- 常用函数
top() pop() push() emplace() empty() size()
- 自定义比较方式
当数据类型并不是基本数据类型,而是自定义的数据类型时,就不能用greater或less的比较方式了,而是需要自定义比较方式// 在此假设数据类型是自定义的水果: struct fruit { string name; int price; }; //有两种自定义比较方式的方法,如下 //1.重载运算符 //重载”<” //若希望水果价格高为优先级高,则 //大顶堆 struct fruit { string name; int price; friend bool operator < (fruit f1,fruit f2) { return f1.peice < f2.price; } }; //若希望水果价格低为优先级高 //小顶堆 struct fruit { string name; int price; friend bool operator < (fruit f1,fruit f2) { return f1.peice > f2.price; //此处是> } }; //2.仿函数 //若希望水果价格高为优先级高,则 //大顶堆 struct myComparison { bool operator () (fruit f1,fruit f2) { return f1.price < f2.price; } }; //此时优先队列的定义应该如下 priority_queue <fruit,vector<fruit>,myComparison> q;
1. 思路
- 出现频率-> 考虑 map
- 有了频率之后,对频率排序 找出前K个高频
- 排序的时候为什么不用快排?
使用快排要将map转换为vector的结构,然后对整个数组进行排序,
而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。 - 复杂度分析
时间复杂度:O(N2 ),其中 N 为数组的长度。
设处理长度为 N 的数组的时间复杂度为 f(N)。由于处理的过程包括一次遍历和一次子分支的递归,
最好情况下,有 f(N)=O(N)+f(N/2),根据 主定理,能够得到 f(N)=O(N)。
最坏情况下,每次取的中枢数组的元素都位于数组的两端,时间复杂度退化为 O(N 2)。
但由于我们在每次递归的开始会先随机选取中枢元素,故出现最坏情况的概率很低。平均情况下,时间复杂度为 O(N)。 - 空间复杂度:O(N)。
哈希表的大小为 O(N),
用于排序的数组的大小也为 O(N),
快速排序的空间复杂度最好情况为 O(logN),最坏情况为 O(N)。