150 逆波兰表达式求值
思考:
这个也是用栈解决非常快的简单题,逆波兰表达式简单来说就是,一个二元运算符负责完成它之前两个数的运算操作。举个例子:4 13 5 / +
第一个运算符“/”前的两个数13和5,可以还原为中缀表达式13/5,然后13/5的结果以及前一个数4作为运算符“+”的两个操作数,最后组成中缀表达式(4+(13/5))。
依据这个规律,利用栈就可以想到:遍历字符串数组tokens,遇到数字则将数压入栈中(需要经过string转int),遇到运算符则取出栈顶两个数字进行对应运算,再将结果压回栈中,作为新的操作数进行后续计算。
我的代码:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> myStack;
for(int i=0;i<tokens.size();i++){
//如果读取到的字符串是数字,则压入栈中
if(tokens[i]!="+"&&tokens[i]!="-"&&tokens[i]!="*"&&tokens[i]!="/"){
myStack.push(stoi(tokens[i]));
//stoi()函数的作用是将string转为int
//int num=stoi(s[i]);
}
//如果读取到运算符,则取出栈顶的两个数进行对应运算,算完的结果压入栈中
//注意运算符是字符串类型,不能作为switch中的参数,必须先转为char类型
else{
int res;
char* p = tokens[i].data();
int num2=myStack.top();
myStack.pop();
int num1=myStack.top();
myStack.pop();
switch(p[0]){
case '+':
res=num1+num2;
break;
case '-':
res=num1-num2;
break;
case '*':
res=num1*num2;
break;
case '/':
res=num1/num2;
break;
}
myStack.push(res);
}
}
return myStack.top();
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
239. 滑动窗口最大值(二刷的时候多关注代码复现)
思考:
梦又碎了,,,队列的题一上来就是重量级,完全没想到居然要用双端队列deque,对STL的认识还是太少了。
这是一个使用单调队列的经典题目。看到这个滑动窗口的模样很容易就想到先进先出的队列,同时这个队列窗口还要边移动边告诉我们里面的最大值是什么。
要完成这样的功能,最好是自己定义这个队列的功能函数。要实现合理地返回最大值、弹出窗口内元素、压入新元素,队列里的元素一定是要排序的,而且要最大值放在出队口,才能知道最大值。
但是窗口没有必要一直持有所有元素,只要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列。
注意,单调队列不是简单地对窗口内的数进行排序,不然和大小顶堆就没区别了。
下面描述维护这个单调队列的过程:
首先确定,这个队列是一个头大尾小的队列。
遍历字符串。第一个窗口时,空窗口则直接压入第一个元素。接下来的元素如果比窗口尾部的元素大,就将尾部元素弹出;直到窗口尾部元素不再比新元素小/窗口为空时,就把新元素从尾部压入窗口。——自定义push操作
当第一个窗口的元素都完成判断后,将窗口头部的数,也就是第一个窗口的最大值加入结果数组中。
之后要加入后面字符串的新字符,需要先处理窗口头部的最大数。卡哥的解法在这个地方很巧妙地将字符串从头一个个判断是否还存在于窗口最前端:
void pop(int num){
if(!que.empty()&&num==que.front())
que.pop_front();
}
————————————————————————————————
myQueue.pop(nums[i-k]);
如果判断条件相等,说明现在窗口是满的,因为没有元素在push阶段被从尾部弹出栈(你细品),必须将头部元素弹出,给下一个压入窗口的新元素腾位置。 (这个地方我是我没想到的)——自定义pop操作
然后继续循环压入新元素,同时将这一轮的窗口内最大值加入结果数组,结束本次循环。
这样描述完,会发现使用deque作为数据结构实现最合适。常用的queue在没有指定容器的情况下,deque就是默认底层容器。
我的代码:
class Solution {
private:
//设计一个只维护头部最大值的单调队列
class diyQueue{
public:
//queue没指定容器时,也是用的deque做底层
//现在deque可以自由使用
deque<int> que;//双端队列,前后都可以压入弹出
int getMaxValue(){
return que.front();
}
//只有 即将加入的元素与队列最前头的元素相同时,才从头部弹出元素
void pop(int num){
if(!que.empty()&&num==que.front())
que.pop_front();
}
//用来作为滑动窗口添加元素的操作
void push(int num){
//如果队列尾部的元素比即将加入的元素小,就将尾部元素先弹出,然后再将新元素加入
//保证维护最前头元素为最大数,且它之后的元素单调
while(!que.empty()&&num>que.back()){
que.pop_back();
}
que.push_back(num);
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
diyQueue myQueue;
//先把队列装满再说,大小为k
for(int i=0;i<k;i++){
myQueue.push(nums[i]);
}
res.push_back(myQueue.getMaxValue());
for(int i=k;i<nums.size();i++){
//先把队列头部的元素弹出,i-k意在从字符串头部逐个判断还是否在队列头部,在就说明队列现在是满的,需要pop操作弹出
myQueue.pop(nums[i-k]);
myQueue.push(nums[i]);
res.push_back(myQueue.getMaxValue());
}
return res;
}
};
*其实还可以用multiset来模拟这个过程,参考代码可见代码随想录的学习网站
- 时间复杂度: O(n)
- 空间复杂度: O(k)——定义一个辅助队列,所以是O(k)
347.前 K 个高频元素
思考:
这道题重点其实在对数据结构的了解和使用(bushi
前k个高频元素,解决的步骤分为下面几步:
1、统计元素出现的次数
2、对次数排序
3、找出次数最多的前k个元素
第一步统计元素出现次数,这类问题用哈希表来解决,特别是map比较合适,元素做key、频率做value,之后要访问也比较方便。
然后对频率进行排序,这个时候可以用一种容器适配器,就是优先级队列(priority_queue)。
*优先级队列本质其实是堆,只是优先级队列对外接口也是队头取元素,队尾添加元素,看起来像一个队列。
优先级队列内部元素自动依照元素权值排列,一般利用大顶堆max-heap完成优先级队列元素的排序。
*关于堆,一种完全二叉树,树中的结点都不大于其左右孩子结点(或不小于其左右孩子结点),前一种就是小顶堆,树顶是最小元素;后一种是大顶堆,树顶是最大元素。
相较于优先级队列,使用快速排序还得将map转换为vector的结构,然后再对整个数组进行排序,不能时刻维护k个元素的有序序列,维护起来会很麻烦。所以用优先级队列最合适。
确定了利用优先级队列后,这时候要思考是用大顶堆还是小顶堆?
这个地方很容易出现思维惯性,既然维持前k个最大数,为什么不选大顶堆呢?虽然大顶堆越靠近顶部元素权值越大,但是大顶堆的维护每次弹出会弹出最大的元素,这样就无法保留前k给高频元素了。
选用小顶堆,不仅更新堆元素时可以优化掉最小的元素,一直保持k个元素,同时最后保留下来的也是所求的前k个高频元素。
关于小顶堆大小比较的代码实现,需要较大元素下沉到堆底部,让最小元素留在堆顶,及时弹出——对应的符号是“>”(相反大顶堆比较操作的符号是“<”):
//创建自定义小顶堆
class cmp{
public:
//返回true的比较元素下沉到堆底部,现在需要让频率低的元素在堆顶,及时弹出
bool operator()(pair<int,int> &left,pair<int,int> &right){
return left.second>right.second;
}
};
在研究解法的时候有参考过这篇文章对优先级队列(priority_queue)的介绍,讲得还挺清楚的。
我的代码:
class Solution {
public:
//创建自定义小顶堆
class cmp{
public:
//返回true的比较元素下沉到堆底部,现在需要让频率低的元素在堆顶,及时弹出
bool operator()(pair<int,int> &left,pair<int,int> &right){
return left.second>right.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//先用哈希表统计元素和对应的频率,元素做key频率做value
//便于之后利用map的数据对频率排序
//欺负我不熟悉STL是吧。
vector<int> res;
unordered_map<int,int> countMap;
for(int i=0;i<nums.size();i++){
countMap[nums[i]]++;
}
//优先级队列的定义:priority_queue<typename, container, functional>
priority_queue<pair<int,int>,vector<pair<int,int>>,cmp> findMaxK;
int count=0;
for(auto elem:countMap){
if(count>=k){
//这个地方必须先push再pop,因为cmp操作的机制是在小顶堆有两个及以上元素的时候进行的两元素比较
//如果k=1时先pop的话堆里就一直只有一个元素,没法进行大小比较,而导致答案错误
findMaxK.push(elem);
findMaxK.pop();
}
else{
findMaxK.push(elem);
count++;
}
}
//优先级队列本质是一个堆,只能顶部吐出,所以取顶部元素用top而不是front
while(!findMaxK.empty()){
res.push_back(findMaxK.top().first);
findMaxK.pop();
}
return res;
}
};
- 时间复杂度: O(nlogk)
- 空间复杂度: O(n)
*文章是本人刷题过程中的一些笔记和理解,记录的解析不一定足够清晰,也可能存在本人暂未意识到的错误,如有问题欢迎大家指出。文章中学习到的解法来自代码随想录的B站视频(栈和队列1~2)以及代码随想录的学习网站。