后缀表达式,简单地说,就是一种运算符在操作数后面的表达式,后缀表达式有个很重要的特点就是可以去掉中缀表达式的括号但是又保留运算的优先级,这样便于计算机计算表达式。而我们数学上使用的是中缀表达式,(表达式不包括双引号)
例如“1+2*(-5)”,把这个表达式转成后缀表达式就是“1 2 -5 * +”。
手动将中缀转后缀的方法这里也说一下,以上面的这个“1+2*(-5)”表达式为例,我们先根据运算的顺序为表达式加上括号,处理成这样“(1+(2*(-5)))”,然后我们根据运算的先后顺序,逐步执行“把运算符放到两个操作数后面的右括号后面,并去掉相应的括号”的操作。按照运算顺序,我们应该先处理表达式最里层的“(2*(-5))”,那么处理后就变成这样“ 2 -5 * ”,此时整个表达式是这样“ (1 + 2 -5 *) ”,(这里其实逻辑上是这样的“ (1 + (2 -5 *)) ”,表示“(2 -5 *)”被当做一个操作数,但是我们不加这个里层的括号也行),我们继续按规则处理,然后变成这样“ 1 2 -5 * + ” ,那么到这里我们就手动把一个中缀表达式转成后缀表达式了。理解手动转换的过程有利于我们后面理解程序的转换逻辑。
先贴一下我实现的ExpParser的代码:https://github.com/Melonl/ExpParser
源码中我简单封装了一个ExpParser类,使用时直接在ExpParser对象上调用parser()方法,将要处理的数学表达式传进去即可得到返回的double类型的结果。源码中的Array和ArrayStack都是我为了练手手写的简陋的动态数组和基于动态数组的栈。下面解释一下相关函数的实现思路和原理。
首先是一些细节的处理函数:
bool isOp(string s)//判断是否是操作符
bool isNumStr(string s)//判断是否是数字
bool checkInput(string &str)//检查输入的字符是否合法,包括括号的匹配
double s2d(string s)//string to double,将一个小数字符串转为double
int getPriority(string opstr)//获取传入的操作符的优先级,返回值越低优先级越高
下面说说核心函数:
首先是将整个表达式string按是否是操作符分割的函数splitExp(),返回分割后的字符串数组,容器为自己实现的Array动态数组
Array splitExp(string s)
{
Array tmp;
for (int i = 0; i < s.size(); i++)
{
string num;
while (isdigit(s[i]) || s[i] == '.')
{
num += s[i];
i++;
}
if (num.size())
{
tmp.addLast(num);
}
if (isOp(s[i]))
{
string ss;
ss += s[i];
tmp.addLast(ss);
}
}
//处理减号
for (int i = 0; i < tmp.getSize(); i++)
{
if (i == 0 && tmp.getSize() > 1 && tmp[i] == "-" && isdigit(tmp[i + 1][0]))
{
tmp[i + 1].insert(0, "-");
tmp.remove(i);
}
else if (i == 0 && tmp.getSize() > 1 && tmp[i] == "-" && tmp[i + 1][0] == '(')
{
string s = "0";
tmp.addFirst(s);
}
else if (tmp[i] == "-" && i != 0 && !isdigit(tmp[i - 1].back()) && i != tmp.getSize() - 1 && isdigit(tmp[i + 1][0]))
{
if (isOp(tmp[i - 1]))
{
tmp[i + 1].insert(0, "-");
tmp.remove(i);
}
}
}
return tmp;
}
这个函数的实现思路是:首先根据运算符和左右括号分割字符串,将运算符和字符串分开,注意,此时不认为小数中的点是运算符,但是负号按照减号处理。然后扫描整个分割好的字符串数组处理负号,逻辑如下:
1.如果“-”出现在表达式首位并且后面紧跟数字,那么此时为负号,合并到后面的数字字符串里去
2.如果“-”出现在表达式首位并且后面紧跟左括号,那么此时为负号,在整个字符串数组首位插个0处理成 “ 0 - (xxx)”的形式
3.如果“-”出现在左括号后面,那么此时为负号,合并到后面的数字字符串中
因为输入的是一个规整的数学表达式,所以我觉得这个逻辑不会有太大的问题。
这个函数把分割好的字符串数组转成后缀表达式,依旧返回一个字符串数组。
关于中缀转后缀的代码写法可以参考这篇博客:https://www.cnblogs.com/whlook/p/7143327.html
这篇博客讲解得非常详细,我自己讲估计讲不了这么清楚,所以还是请各位去看这篇博客吧。我的代码也是参考这篇博客的讲解实现的,贴下我的实现代码:
Array toSuffixExp(Array &expstr)
{
ArrayStack opstk;
ArrayStack outstk;
for (int i = 0; i < expstr.getSize(); i++)
{
if (isNumStr(expstr[i]))
{
outstk.push(expstr[i]);
continue;
}
else
{
if (expstr[i][0] == '(')
{
opstk.push(expstr[i]);
continue;
}
else if (expstr[i][0] == ')')
{
while (1)
{
if (opstk.top()[0] == '(')
{
opstk.pop();
break;
}
else
{
outstk.push(opstk.pop());
}
}
continue;
}
else if (opstk.size() == 0 || getPriority(expstr[i]) < getPriority(opstk.top()) || opstk.top() == "(")
{
opstk.push(expstr[i]);
continue;
}
else if (isOp(expstr[i]) && getPriority(expstr[i]) >= getPriority(opstk.top()))
{
while (opstk.size() != 0 && getPriority(expstr[i]) >= getPriority(opstk.top()))
{
outstk.push(opstk.pop());
}
opstk.push(expstr[i]);
}
}
}
int tmpsz = opstk.size();
for (int i = 0; i < tmpsz; i++)
{
outstk.push(opstk.pop());
}
ArrayStack tmpstk;
Array res;
int size = outstk.size();
for (int i = 0; i < size; i++)
{
tmpstk.push(outstk.pop());
}
while (tmpstk.size() != 0)
{
res.addLast(tmpstk.pop());
}
return res;
}
这个函数实现了对后缀表达式的计算,具体的逻辑还是很简单的,要注意下参数的num2才是第一个操作数,num2是第二个操作数。代码如下
string cal(string &num2, string &num1, string &op)
{
//cout << "cal:" << num1 << "," << num2 << endl;
double n1, n2, res;
if (op.size() != 1)
{
cout << "cal error: " << op << endl;
return "";
}
switch (op[0])
{
case '+':
res = s2d(num1) + s2d(num2);
break;
case '-':
res = s2d(num1) - s2d(num2);
break;
case '*':
res = s2d(num1) * s2d(num2);
break;
case '/':
res = s2d(num1) / s2d(num2);
break;
case '^':
res = pow(s2d(num1), s2d(num2));
break;
default:
cout << "cal error: not find operator for " << op[0] << endl;
break;
}
return to_string(res);
}
总的来说后缀表达式的计算还是比较简单的,只是逻辑稍微繁琐了点 ,耐心理清步骤就可以实现出来,关于ExpParser这个项目,后期我可能会尝试加入对 对数、根号、求余、阶乘等运算的支持,感觉有帮助的话不妨在github star一下我的项目,感谢阅读。