文章目录
- stack
- stack的基本函数接口
- 关于栈的习题
- [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/)
- [150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/)
- [215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/)
- [JZ31 栈的压入、弹出序列](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking)
stack
学习stack的使用首先就是文档
首先可以看到stack这个容器的模板参数比以前的vector和list等容器改变了一个参数,以前的第二个参数是alloc
也就是空间配置器(内存池),而stack的这个container是一个新的名词,叫做容器适配器。说到容器适配器,实际就是将其他容器作为底层容器,比如后面给的缺省参数deque是双端队列,当然我们也可以换成其他容器比如list或者vector都是可以的。
为什么叫做容器适配器,这里可以类比与电源适配器,电源适配器就是将我们220v的交流电转换成我们需要的电。容器适配器也是一样的。通过对其他容器的封装实现了栈的后进先出的功能。这个部分在模拟代码哪里可以清楚看到。
说到了容器适配器,现在我们需要整体了解一下STL的六大组件。
1.容器(是常见的数据结构:string,vector,list…)
2.算法(常用算法sort,reverse,find…)
3.迭代器(不关心底层容器的实现结构,使用简单统一的方式来访问和修改容器)
4.适配器(就是在某些底层容器加以封装形成其他容器的特性)
5.仿函数(使用类重载操作符,使得这个类的对象可以像函数一样使用)
6空间配置器(内存池)
stack的基本函数接口
这里的emplace功能上是和push一样的,但是引入了C++11里面的右值引用。
这里主要看一下构造函数
这里的container_type就是第二个模板参数容器适配器,所以这里的构造函数,如果我们第二个参数用的是缺省参数deque,可以直接使用deque来拷贝构造一个stack对象。下面我是用的vector,道理是一样的。
void test2()
{
vector<int> vv(10, 6);
stack<int,vector<int>> stk2(vv);
//拷贝构造也是可以使用的。
stack<int, vector<int>> stk(stk2);
cout << stk.size() << endl;
while (!stk.empty())
{
cout << stk.top() << " ";
stk.pop();
}
cout << stk.size() << endl;
}
虽然构造函数里面并没有标注拷贝构造等等以前的那些构造函数,但是只要是你使用的那个容器适配器可以使用的构造函数,stack也是可以一起使用的。
下面就来看一下模拟实现stack的代码
//stack.h
#pragma once
#include<vector>
namespace xzj
{
template<class T,class Container = std::vector<T>>
class stack
{
public:
void push(const T& x)
{
_a.push_back(x);
}
void pop()
{
_a.pop_back();
}
bool empty()
{
return _a.empty();
}
T& top()
{
return _a.back();
}
const T& top() const
{
return _a.back();
}
size_t size()
{
return _a.size();
}
private:
Container _a;
};
}
这是最简单的容器适配器的使用,这里使用的是vector,也可以换成list等等,当天也可在类模板实例化的时候指定底层的容器适配器是什么容器。
关于栈的习题
下面来看几道栈的习题的应用。
232. 用栈实现队列
解题思路:
此题可以用两个栈实现,一个栈进行入队操作,另一个栈进行出队操作出队操作: 当出队的栈(skpop)不为空的时候,直接进行出栈操作,如果为空,需要把入队的栈(skpush)元素全部导入到出队的栈,然后再进行出栈操作
计算有效元素个数,就是这两个栈的有效元素个数之和
class MyQueue {
public:
stack<int> skpush;
stack<int> skpop;
MyQueue() {
}
void push(int x) {
skpush.push(x);
}
int pop() {
if(skpop.empty())
{
while(!skpush.empty())
{
skpop.push(skpush.top());
skpush.pop();
}
}
int tmp = skpop.top();
skpop.pop();
return tmp;
}
int peek() {
if(skpop.empty())
{
while(!skpush.empty())
{
skpop.push(skpush.top());
skpush.pop();
}
}
return skpop.top();
}
bool empty() {
return skpop.empty() && skpush.empty();
}
};
150. 逆波兰表达式求值
思路:逆波兰表达式是将操作符按照优先级排列了,所以遍历数组,遇到数字就入栈,遇到操作符就取栈顶的两个数字进行运算将结果再入栈(要注意先出栈的是右操作符)最后栈内剩下的那个数字就是最后的结果。
class Solution {
public:
void GetNum(stack<int>& stk,int& left,int& right)
{
right = stk.top();
stk.pop();
left = stk.top();
stk.pop();
}
int evalRPN(vector<string>& tokens) {
stack<int> stk;
int left = 0,right = 0;
for(const auto& ch : tokens)
{
switch(ch.back())
{
case '+':
GetNum(stk,left,right);
stk.push(left + right);
break;
case '-':
GetNum(stk,left,right);
stk.push(left - right);
break;
case '*':
GetNum(stk,left,right);
stk.push(left * right);
break;
case '/':
GetNum(stk,left,right);
stk.push(left / right);
break;
default:
stk.push(stoi(ch));
break;
}
}
return stk.top();
}
};
逆波兰表达式又叫做后缀表达式,就是操作符是在操作数的后面。我们平时使用的都是中缀表达式,也即是操作符的两端是操作数。
上面题目的思路是将后缀表达式转换成中缀表达式计算。那么我们如何将中缀表达式转换成后缀表达式呢?
中缀表达式转后缀表达式:
1.首先遇到数字就放进新的vector容器内,遇到运算符就要开始判断,1.如果存放运算符的栈为空,那么将运算符入栈。2.如果该运算符比栈顶的运算符优先级更高,那么将该运算符入栈。3.如果该运算符的优先级低于栈顶的运算符,那么出栈顶的运算符,该运算符继续和栈顶的运算符比较,直到该运算符优先级大于栈顶元素,或者栈为空,就将该运算符入栈。
代码如下(没有包含括号的情况,如果遇到括号,需要设计一个标志位,标记括号内的运算符优先级大于括号外的优先级):
void Infix_to_Suffix(vector<string>& fix)
{
vector<string> ans;
stack<string> stk;
for (auto str : fix)
{
switch (str.back())
{
case '+':
{
if (stk.empty())
stk.push(str);
else
{
string& tmp = stk.top();
while (stk.empty() && (tmp.back() == '*' || tmp.back() == '/'))
{
ans.push_back(stk.top());
stk.pop();
tmp = stk.top();
}
stk.push(str);
}
break;
}
case '-':
{
if (stk.empty())
stk.push(str);
else
{
string& tmp = stk.top();
while (stk.empty() && (tmp.back() == '*' || tmp.back() == '/'))
{
ans.push_back(stk.top());
stk.pop();
tmp = stk.top();
}
stk.push(str);
}
break;
}
case '*':
{
stk.push(str);
break;
}
case '/':
{
stk.push(str);
break;
}
default:
ans.push_back(str);
break;
}
}
while (!stk.empty())
{
ans.push_back(stk.top());
stk.pop();
}
for (auto& ss : ans)
{
cout << ss << " ";
}
}
void test3()
{
vector<string> fix{ "4","+","13","/","5" };
Infix_to_Suffix(fix);
}
215. 数组中的第K个最大元素
思路:使用优先级队列,优先级队列实际就是将vector封装然后加上堆的算法,实际就是一个堆,用nums构建一个优先级队列,默认是大堆,但是这里需要小堆,用前k个数字构建一个小堆,然后遍历剩下的数字,如果遇到比堆顶元素大的那么就删除堆顶元素然后插入新元素,最后这个堆里面的k个数就是整个nums里面最大的k个,第k大的数字就是这个堆里面最小的数字。所以最后返回堆顶的数字。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue pq(nums.begin(),nums.begin()+k,greater());
for(size_t i = k;i<nums.size();i++)
{
if(pq.top() < nums[i])
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
JZ31 栈的压入、弹出序列
思路:遍历pushV同时遍历popV将pushV中的数字插入到栈内,如果栈顶数字和popV的数字相同则出栈,popV走到下一个数字,继续比较这需要写一个循环,直到栈为空,或者两者不相等就停止,因为可能遇到连续出栈的情况所以写成循环。
最后若是popV走到了结尾,那么就是一个合法的弹出序列否则就是不合法的。
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
stack<int> stk;
int i = 0;
for(auto e : pushV)
{
stk.push(e);
while(!stk.empty() && stk.top() == popV[i])
{
stk.pop();
i++;
}
}
return i == popV.size();
}
};