后缀表达式又称为逆波兰表达式,在计算机解析算式字符串上有很大的应用。比如大家熟知的Python
和JavaScript
中的eval()
函数,可以很方便地计算算式字符串的值:
>>> infix = "1+(2+4)*8"
>>> eval(infix)
49
这个强大的功能便是通过后缀表达式实现的,下面,我们可以在C++中实现这个函数:
首先导入需要的库:
#include <iostream>
#include <stdio.h>
#include <sstream>
#include <string>
#include <vector>
#include <map>
using namespace std;
然后,声明几个全局变量并实现几个简单的函数:
map<string, int> op_mapping; // 运算符优先级映射表
string ops = "+-*/()"; // 包含所有运算符的字符串,为了通过find方法快速判断目标字符是否为操作符
bool ValueError = false; // 是否发生数值错误的标记
bool ExpressionError = false; // 是否发生表达式错误的标记
// 将string转换成double
double toDouble(string str)
{
double target;
stringstream ss;
ss << str;
ss >> target;
return target;
}
// 初始化操作符优先级映射表
void init_mapping(map<string, int> &mapping)
{
mapping["+"] = 0;
mapping["-"] = 0;
mapping["*"] = 1;
mapping["/"] = 1;
mapping["("] = 2;
mapping[")"] = 2;
}
init_mapping()
和变量op_mapping
是为了快速比较两个运算符的优先级大小。当然,如果你不是通过这个方法来比较运算符优先级的话,你完全可以不这么写。
中缀表达式转后缀表达式
接下来,就是核心部分的编写了,首先,我们需要将中缀表达式转为后缀表达式。相信学过数据结构的读者应该对这个逻辑不陌生,中缀表达式转为后缀表达式是通过栈来完成的(也可以通过树)。我们需要两个栈,存放最终结果的result
和临时存放操作符的op_stack
。
基本过程为,从左到右遍历中缀表达式的每个字符,通过ops.find()
我们可以快速判别这个字符代表操作数还是操作符,如果是操作数,则将这个数字push_back()
进结果栈result
;如果是操作符,记为cur_op
,则进行以下判断:
op_stack
为空,则cur_op
直接入栈。cur_op=="("
,则cur_op
直接入栈。cur_op==")"
,则进行如下操作:开始将op_stack
中的元素弹出,并将弹出的元素放入result
中,直到弹出的元素为(
,停止弹出,并将弹出的(
与当前元素cur_op
(也就是)
)丢弃。op_stack.back()=="("
且cur_op
不为括号,则cur_op
直接入栈。- 在
cur_op
与op_stack.back()
都不为括号的情况下,cur_op
的优先级大于op_stack.back()
,则cur_op
直接入栈。 - 在
cur_op
与op_stack.back()
都不为括号的情况下,cur_op
的优先级小于等于op_stack.back()
,则将op_stack
中元素逐一弹出,直到cur_op
的优先级大于op_stack
的栈顶元素,或者栈内为空,停止弹出,cur_op
入栈。
中缀表达式遍历完后,将op_stack
中的剩余元素逐一弹出,并push_back()
进result
。
举个简单的例子,我们目前的中缀表达式如下:
infix expression
=
5
+
(
1
+
2
)
∗
4
−
3
\text{infix expression}=5+(1+2)*4-3
infix expression=5+(1+2)∗4−3
下面演示一下手动中缀转后缀:
潦草警告=_=
1.扫到5
,判断得5
是操作数,则5
入栈result
:
2.扫到+
,判断得+
是操作符,此时op_stack
为空,则+
直接入栈op_stack
:
3.扫到(
,判断得(
是操作符,因为cur_op=="("
,所以直接入栈op_stack
:
4.扫到1
,判断得1
是操作数,所以直接入栈result
:
5.扫到+
,判断得+
是操作符,由上述第四条判断,由于+
不是括号,而op_stack
的栈顶元素是(
,所以+
直接入栈op_stack
:
6.扫到2
,判断得2
是操作数,所以直接入栈result
。
7.扫到)
,判断得)
是操作符,则将op_stack
中的元素弹出并push_back
进result
。将剩余的(
和输入的)
丢在一边:
8.扫到*
,判断得*
是操作符,且优先级大于身为op_stack
的栈顶元素+
,因此,直接入栈op_stack
:
9.扫到4
,判断得4
是操作数,直接入栈result
:
10.扫到-
,判断得-
是操作符,此时发现op_stack
的栈顶元素*
优先级大于-
,则将*
移到result
中;接下来发现op_stack
的栈顶元素+
优先级等于-
,则将+
移到result
中;此时op_stack
已为空,停止弹出,并将-
入栈op_stack
:
11.扫到3
,判断得3
是操作数,直接入栈result
:
12.中缀表达式遍历结束,将op_stack
中剩余元素逐一移到result
中,再将元素从栈底逐个读出,得到的便是后缀表达式了:
postfix expression
=
5
1
2
+
4
∗
+
3
−
\text{postfix expression}= 5\ 1\ 2 \ +\ 4\ *\ +\ 3\ -
postfix expression=5 1 2 + 4 ∗ + 3 −
当然,为了程序的稳健性,我们需要在多个节点做判断。比如如果用户输入的是一个非法的中缀表达式
5
+
1
+
2
)
∗
4
−
3
5+1+2)*4-3
5+1+2)∗4−3,少了左括号,那么在读入)
时,程序会将元素逐个弹出,直到栈顶元素为(
。因此,一旦扫到(
,则会导致栈内所有操作符被弹出,继而出错。所以我们需要在一些关键节点做判断,尤其是在使用.back()
方法获取栈顶元素时,一定要在可能报错的地方判断栈是否为空。
除此之外,扫描数字的逻辑也值得注意。我们上面的数字都是一个字符就可以表示,因此,逐一扫描数字,再push_back()
进result
中没错,但是一旦我们的操作数出现了34,251这样字符串时,读到一个字符就直接push_back()
进result
中就不对了,我们需要一个string cur_num
来临时保存读到字符,等到251读全了,再将cur_num
push_back()
进result
,然后清空cur_num
就行了。
程序如下:
// 将中缀表达式转化为后缀表达式
vector<string> toPostfix(string formula)
{
vector<string> result;
vector<string> op_stack;
string cur_num, cur_op;
for (int i = 0; i < formula.size(); ++ i)
{
if (ops.find(formula[i]) == ops.npos) // 扫描到的是操作数
cur_num += formula[i];
else // 扫描到的是操作符,现将累加的操作数字符串加入
{
if (!cur_num.empty())
{
result.push_back(cur_num);
cur_num.clear();
}
cur_op = formula[i];
if (op_stack.empty()) // 栈为空,直接入栈
op_stack.push_back(cur_op);
else if (cur_op == "(") // 当前操作数为左括号,直接入栈
op_stack.push_back(cur_op);
else if (cur_op == ")") // 当前操作数为右括号,则需要将op_stack中直到左括号前的所有的元素弹出
{
while (op_stack.back() != "(")
{
result.push_back(op_stack.back());
op_stack.pop_back();
if (op_stack.empty()) // 不合法的表达式会出现这样的情况
{
ExpressionError = true;
result.push_back("0");
return result;
}
}
op_stack.pop_back(); // 将左括号弹出
}
else if (op_stack.back() == "(") // 在当前操作符不是括号的情况下,如果栈顶元素为左括号,则直接入栈
op_stack.push_back(cur_op);
else if (op_mapping[cur_op] > op_mapping[op_stack.back()]) // 在当前操作符和栈顶元素为+-*/的情况下,若当前操作符优先级大于栈顶元素,直接入栈
op_stack.push_back(cur_op);
else // 最后一种情况就是当前操作符的优先级低于或等于栈顶元素优先级时
{
while ((op_stack.back() != "(") && (op_mapping[op_stack.back()] >= op_mapping[cur_op]))
{
result.push_back(op_stack.back());
op_stack.pop_back();
// 若栈已空,则直接返回
if (op_stack.empty())
break;
}
op_stack.push_back(cur_op); // 符合要求的操作符弹出后,当前操作符入栈
}
}
}
result.push_back(cur_num);
// 最后op_stack可能还会有剩余元素,全部弹出
while(!op_stack.empty())
{
result.push_back(op_stack.back());
op_stack.pop_back();
}
return result;
}
计算后缀表达式
计算后缀表达式就简单了,我们之所以将中缀表达式转换成后缀表达式就是因为后缀表达式好算。后缀表达式没有括号,运算符优先级体现在了自身结构上。
具体计算很简单:我们建一个栈result
。然后从左往右扫描后缀表达式,遇到操作数,就入栈;遇到操作符op
,就让栈顶的两个元素num1
和num2
出栈,假设在栈中时num1
在num2
的上面,则计算num2 op num1
,将所得结果入栈。最终栈内只会剩下一个元素,这个元素便是答案。
很显然,如果原本输入的中缀表达式不合法,则会出现两种隐患:
- 在获取
num1
和num2
时,若栈内元素不足两个,会出错。 - 最终得到的
result
中的元素数量大于1。
我们可以在这些节点上特判。程序如下:
double calculatePostfix(vector<string> &postfix)
{
vector<double> result;
for (int i = 0; i < postfix.size(); ++ i)
{
if (ops.find(postfix[i]) == ops.npos) // 扫描到操作数,直接压入栈中
result.push_back(toDouble(postfix[i]));
else // 扫描到操作符
{
// 如果剩余元素的数量小于2,则表达式非法
if (result.size() < 2)
{
ExpressionError = true;
return 0.0;
}
double num1 = result.back();
result.pop_back();
double num2 = result.back();
result.pop_back();
double op_res;
// 分类讨论,计算num2 op num1
if (postfix[i] == "+")
op_res = num2 + num1;
else if (postfix[i] == "-")
op_res = num2 - num1;
else if (postfix[i] == "*")
op_res = num2 * num1;
else if (postfix[i] == "/")
{
// 此处需要判断一下是否分母为0
if (num1 == 0)
{
ValueError = true;
return 0.0;
}
op_res = num2 / num1;
}
// 将所的结果重新压入栈中
result.push_back(op_res);
}
}
if (result.size() == 1) // 返回栈顶元素(如果是合法的表达式,则此时栈中只有一个元素)
return result.back();
else // 不合法的表达式会导致结束时,result中有不止一个元素
{
ExpressionError = true;
return 0.0;
}
}
拼出最后的eval()函数
将得到后缀表达式的函数和计算后缀表达式的函数一整合就可以得到eval()
函数了:
double eval(string &infix)
{
vector<string> postfix = toPostfix(infix);
return calculatePostfix(postfix);
}
最后我们可以测试一下:
int main()
{
double result;
init_mapping(op_mapping); // 初始化映射表
string infix = "1+2*(3+4)-5";
cout << eval(infix);
return 0;
}
out:
10
符合预期。