✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
贝蒂的主页:Betty’s blog
1. stack与queue的介绍
1.1. stack
stack
就是我们数据结构中所学习的栈,它遵循后进先出(Last In First Out,LIFO)的原则。具体来说就就是STL
对与栈这个数据结构的封装,使用时包含头文件#include<stack>
即可。
1.2. queue
queue
就是我们数据结构中所学习的队列,遵循先进先出(First-In-First-Out,FIFO)的原则。具体来说就就是STL
对与队列这个数据结构的封装,使用时包含头文件#include<queue>
即可。
1.3. 容器适配器
在谈容器适配器之前,我们先来谈谈什么是适配器。适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
这中模式就在我们STL
中的stack
,queue
得到了适用。为了防止重复造轮子,stack
,queue
在实现时就复用了STL
中的其他容器。我们打开相关文档就知道stack
与queue
有两个模版参数,一个是容器的类型,一个就是容器适配器。
其中需要注意的就是:
stack
的容器适配器需要支持empty
,size
,back
,push_back
,pop_back
五种功能,其中常见的容量有vector
,deque
,list
。stack
的容器适配器需要支持empty
,size
,front
,back
,push_back
,pop_front
五种功能,其中常见的容量有deque
,list
。
那么为什么STL
库中会选择deque
作为stack
与queue
的默认容器呢?原因如下:
deque
适用于stack
的原因:
deque
头插头删、尾插尾删效率高,契合stack
的操作需求。- 元素增长时,
deque
比vector
效率高,因为扩容时不需要搬移大量数据。
deque
适用于queue
的原因:
- 头插头删、尾插尾删效率高,符合
queue
的操作特性。- 元素增长时,不仅效率高,而且相比
list
内存使用率高,因为list
每个节点有额外指针开销,内存不够紧凑。并且大量数据时,list
容易造成内存碎片。
2. stack与queue的使用
2.1. stack的使用
stack
提供的接口相比较与其他容量明显较少,这是由于其结构决定的,并且因为stack
也并不需要遍历,所以也不存在迭代器的概念。
以下就是stack
的接口:
因为我们学习过数据结构中的栈,所以使用这里的接口的成本也比较低
void Test1()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
cout << "容量大小为:" << st.size() << endl;
cout << "出栈顺序为:";
while (!st.empty())
{
int top = st.top();
st.pop();
cout << top << " ";
}
cout << endl;
}
当然我们也可以选择其他容器作为容器适配器。
void Test2()
{
//选用vector作为容器适配器
stack<int,vector<int>> st1;
st1.push(1);
st1.push(2);
st1.push(3);
st1.push(4);
st1.push(5);
cout << "出栈顺序为:";
while (!st1.empty())
{
int top = st1.top();
st1.pop();
cout << top << " ";
}
cout << endl;
//选用list作为容器适配器
stack<int, list<int>> st2;
st2.push(1);
st2.push(2);
st2.push(3);
st2.push(4);
st2.push(5);
cout << "出栈顺序为:";
while (!st2.empty())
{
int top = st2.top();
st2.pop();
cout << top << " ";
}
}
2.2. queue的使用
queue
提供的接口相比较与其他容量明显较少,这是由于其结构决定的,并且因为queue
也并不需要遍历,所以也不存在迭代器的概念。
以下就是queue
的接口:
因为我们学习过数据结构中的队列,所以使用这里的接口的成本也比较低
void Test3()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
cout << "容量大小为:" << q.size() << endl;
cout << "队首:" << q.front() << endl;
cout << "队尾:" << q.back() << endl;
while (!q.empty())
{
int val = q.front();
q.pop();
cout << val << " ";
}
cout << endl;
}
当然我们也可以选择其他容器作为容器适配器。
void Test4()
{
queue<int,list<int>> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while (!q.empty())
{
int val = q.front();
q.pop();
cout << val << " ";
}
cout << endl;
}
3. stack与queue的模拟实现
3.1. 模拟实现stack
stack
借助容器适配器的话实现就非常简单,我们只需要复用接口即可,一般我们将容器的末尾作为栈顶。
#include <deque>
namespace betty
{
//默认使用deque适配器
template <class T, class Container = deque<T>>
class stack
{
public:
//判断是否为空
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
//取栈顶数据
const T& top()
{
return _con.back();
}
//压栈
void push(const T& x)
{
_con.push_back(x);
}
//出栈
void pop()
{
_con.pop_back();
}
private:
Container _con;
};
}
3.2. 模拟实现queue
queue
借助容器适配器的话实现也非常简单,我们只需要复用接口即可,一般我们将容器的末尾作为队尾,容器的起始作为队头。
namespace betty
{
//默认使用deque适配器
template <class T, class Container = deque<T>>
class queue
{
public:
//判断是否为空
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
//取队头数据
const T& front()
{
return _con.front();
}
//取队尾数据
const T& back()
{
return _con.back();
}
//队尾入数据
void push(const T& x)
{
_con.push_back(x);
}
//队头出数据
void pop()
{
_con.pop_front();
}
private:
Container _con;
};
}
4. 逆波兰表达式
逆波兰表达式简介逆波兰表达式,也叫后缀表达式,是一种将运算符放在操作数后面的表达式表示方法。 在常规的中缀表达式中,运算符位于操作数之间,例如 2 + 3
。而在逆波兰表达式中,会表示为 2 3 +
。 其优点在于计算时无需考虑运算符的优先级,直接从左到右依次计算即可。例如计算 3 4 + 5 *
,先计算 3 + 4
得到 7
,再计算 7 * 5
得到最终结果 35
。
对于计算机来说常规的中缀表达式是无法直接计算的,因为涉及优先级问题。所以为了让计算机能够直接运算,我们首先要把中缀表达式转为逆波兰表达式。
4.1. 中缀转后缀
中缀表达式转后缀表达式的方法如下:
- 初始化一个栈,用于保存暂时不能确定运算顺序的运算符。
- 从左到右处理各个元素,直到末尾,可能遇到以下三种情况:
遇到操作数,直接输出(加入后缀表达式)。
遇到界限符即
()
:
- 遇到
(
,不参与优先级比较直接入栈。- 遇到
)
,要参与比较,认为)
的优先级最低,依次pop
掉栈顶的运算符输出(加入后缀表达式),直至遇到(
停止,(
直接pop
掉不输出。遇到运算符/操作符:
- 如果是
-
先判断是操作符还是负数,如果是负数输出0
与入栈-
相当于将负数转化为正数。- 如果栈为空,直接入栈,认为
()
优先级最低。- 如果栈不为空,跟栈顶操作符进行优先级比较,栈顶是
(
也比较:
- 比栈顶操作符优先级高,入栈,然后处理下一个元素。
- 比栈顶操作符优先级低或相等,
pop
掉栈顶操作符并输出(加入后缀表达式)。
- 最后遍历完所有元素,再去判断栈是否为空。不为空则依次
pop
掉栈顶的所有运算符输出(加入后缀表达式)。例如,对于中缀表达式
2 + 3 * ( 4 - 1 )
,按照上述规则进行转换。首先处理数字 2 和 3,直接输出。遇到+
,栈为空,入栈。遇到*
,此时栈顶为+
,*
优先级高,入栈。遇到(
,入栈。遇到 4 和 1 直接输出。遇到-
,栈顶为(
,入栈。处理完4 - 1
后,遇到)
,开始pop
栈顶运算符输出,直到遇到(
停止。最终得到后缀表达式2 3 4 1 - * +
// 比较运算符的优先级,返回 1 表示 '+' 或 '-',返回 2 表示 '*' 或 '/'
int precedence(char op) {
if (op == '+' || op == '-')
return 1;
if (op == '*' || op == '/')
return 2;
return 0;
}
// 将中缀表达式转换为后缀表达式的函数,并返回一个 vector<string>
vector<string> infixToPostfix(string& infix)
{
stack<char> opStack; // 用于存储运算符的栈
vector<string> postfix; // 存储转换后的后缀表达式
int i = 0;
bool isPrevOperand = false; // 标记前一个元素是否为操作数
//false为负数,true为操作符-
while (i < infix.size())
{
//负数
if (infix[i] == '-'&&isPrevOperand==false)
{
i++;
postfix.push_back("0");
opStack.push('-');
isPrevOperand = true;//0作为操作数
}
char c = infix[i];
if (isspace(c)) { // 如果是空格则跳过
i++;
continue;
}
if (isdigit(c))
{
string num;
//判断是否是多位数
while (i < infix.size() && isdigit(infix[i])) {
num += infix[i];
i++;
}
postfix.push_back(num);
isPrevOperand = true;//这个多位数作为操作数
}
else if (c == '(') { // 如果是 '('
opStack.push(c);
i++;
isPrevOperand = false;//(相当于表达式开始
}
else if (c == ')') { // 如果是 ')'
while (!opStack.empty() && opStack.top() != '(') {
string op;
op += opStack.top();
postfix.push_back(op);
opStack.pop();
}
if (!opStack.empty() && opStack.top() == '(') {
opStack.pop();
}
i++;
isPrevOperand = true;//()的结果当做操作数
}
else { // 如果是运算符
while (!opStack.empty() && precedence(opStack.top()) >= precedence(c)) {
string op;
op += opStack.top();
postfix.push_back(op);
opStack.pop();
}
opStack.push(c);
i++;
}
}
//剩余操作符依次弹出
while (!opStack.empty()) {
string op;
op += opStack.top();
postfix.push_back(op);
opStack.pop();
}
return postfix; // 返回转换后的后缀表达式
}
问题一:为什么将负数转换为0-正数
?
在处理中缀表达式时,负数的情况可能会带来一些麻烦。例如,单纯的
-3
这样的表达式,如果按照常规方式理解,会有一个操作符和一个操作数。然而,这样在利用后缀表达式进行计算时会变得难以处理。 为了更有效地处理这种情况,我们采取一种优化策略,即将其转化为0 - 正数
的形式。通过这样的转换,就能保证每个操作符都对应两个操作数,使得后续的计算更加清晰和准确。 比如,对于表达式-3
,我们将其转换为0 - 3
。这种处理方式有助于在使用后缀表达式进行计算时,避免因负数引起的逻辑复杂性和错误。
问题二:为什么比栈顶运算符优先级高就入栈,反之则弹出栈顶运算符?
当遇到一个新的运算符时,如果其优先级比栈顶运算符的优先级低,这就意味着前一个表达式应该先进行运算。而如果新运算符的优先级比栈顶运算符的优先级高,由于无法确定后续是否还会有更高优先级的运算符出现,所以不能确定当前新运算符对应的表达式何时运算,此时应将其继续入栈。 例如,栈顶是
+
,新运算符是*
,因为*
的优先级高,所以先将*
入栈;但如果新运算符是-
,其优先级低于*
,则先运算栈顶的*
相关的表达式。
4.2. 计算后缀表达式
在学习完如何将中缀表达式转化为后缀表达式之后,接下来让我们来学习如何运算后缀表达式。
后缀表达式的运算过程如下:
- 首先,创建一个栈
st
用于存储操作数。- 遍历后缀表达式的每个元素(存储在
tokens
向量中)。- 如果元素是运算符(
+
、-
、*
、/
):
- 从栈中弹出两个操作数,分别记为
right
和left
。- 根据运算符进行相应的计算(加法、减法、乘法、除法)。
- 将计算结果压入栈中。
如果元素是操作数(数字),将其转换为整数并压入栈中。
遍历结束后,栈顶元素即为后缀表达式的计算结果,返回该值。
例如,对于后缀表达式
["2", "1", "+", "3", "*"]
,首先将2
和1
压入栈,遇到+
时弹出1
和2
计算2 + 1 = 3
并压入栈,再遇到3
压入栈,遇到*
时弹出3
和3
计算3 * 3 = 9
,最终栈顶元素9
即为结果。
// 计算逆波兰表达式(后缀表达式)的函数
int evalRPN(vector<string>& tokens) {
stack<int> st; // 创建一个用于存储操作数的栈
for (auto& str : tokens) { // 遍历后缀表达式的每个元素
if (str == "+" || str == "-" || str == "*" || str == "/") { // 如果是运算符
int right = st.top(); // 获取栈顶元素作为右操作数
st.pop(); // 弹出栈顶元素
int left = st.top(); // 再次获取栈顶元素作为左操作数
st.pop(); // 弹出栈顶元素
switch (str[0]) { // 根据运算符进行计算
case '+':
st.push(left + right); // 执行加法并将结果入栈
break;
case '-':
st.push(left - right); // 执行减法并将结果入栈
break;
case '*':
st.push(left * right); // 执行乘法并将结果入栈
break;
case '/':
st.push(left / right); // 执行除法并将结果入栈
break;
}
}
else { // 如果是操作数
st.push(stoi(str)); // 将操作数转换为整数并入栈
}
}
return st.top(); // 返回栈顶元素,即计算结果
}
h(left - right); // 执行减法并将结果入栈
break;
case '*':
st.push(left * right); // 执行乘法并将结果入栈
break;
case '/':
st.push(left / right); // 执行除法并将结果入栈
break;
}
}
else { // 如果是操作数
st.push(stoi(str)); // 将操作数转换为整数并入栈
}
}
return st.top(); // 返回栈顶元素,即计算结果
}