算术表达式求值是数据结构中栈运用的典型例子。一般来说,为了算法的简化和代码可读性方面的考虑,都是把我们所习惯写的中缀表达式转化为后缀表达式后,再进行相关的计算,各种计算器程序的原理也与之类似。
本次博客主要讲述两个方面的内容:中缀表达式向后缀表达式之间转化的算法,以及后缀表达式计算求值的算法。
什么是中缀表达式、后缀表达式
像加减乘除以及乘方、求余等这类运算符,在运算是必须有两个或两个以上的操作数参与时才能正常进行的,称为双目运算符。设A和B为某个双目运算符‘#’的两个操作数,则有下列定义:
#AB | 前缀表达式形式 |
---|---|
A#B | 中缀表达式形式 |
AB# | 后缀表达式形式 |
所以看来,中缀表达式和后缀表达式是针对表达式中有双目运算符时才有的说法。所谓的前中后缀,是针对双目运算符在两操作数之间的位置而命名的。
中缀表达式转化为后缀表达式
算术表达式在求值的时候,一般先得进行表达式形式的转化,即把一般的中缀表达式转化为后缀表达式。其转化方法分人工转化和机器转化两种。
设A,B,D,E为四个一位整数,待转化的表达式为 X == A+B*(E+D)
-
人工转化
先将中缀表达式中缺少的括号补齐:
X == A+B*(E+D) —> X == ((A)+(B)*((E)+(D)))
补齐后的表达式X中,一对“()”括起来的数,就是一个操作数,这样做的目的,是为了确定后缀表达式中两个操作数和后面的运算符再从左往右开始,依次扫描第一步中得到的中缀表达式,遇到形如“(Y)”的形式时,先将Y写上,再找后面的操作数,上面的例子里,先写入的字母就应该是A。再看A之后的操作数,整体上来说是“
B*(E+D)
”,此时我们需要对这个整体再用依次之前的法则,先写入的就是B,然后写入E、D。到此为止,后缀表达式形式是“ABED...
”现在来看到
(E+D)
,这个中缀表达式的后缀表达式形式是“ED+
”,所以此时中缀表达式形式是“ABED+”,再看到“B*(E+D)
”这个整体,其中的操作符是‘*
’,所以此整体的后缀表达式是“BED+*
”,到了这一步,后缀表达式的形式是“ABED+*
”。最后看到A+B*(E+D)
这个整体,运算符是‘+
’,所以最终得到的后缀表达式是: “ABED+*+
”X == A+B*(E+D)
—>X == ((A)+(B)*((E)+(D)))
—>X == ABED+*+
此方法,人工转化起来很简单,读者没有听懂的话,可以再看看下面的几个转化案例:X==(2*(3+1)/4)+8+6*9/3
–>X==231+*4/8+69*3/+
X==(1+2)/3*9+8/2
–>X==12+3/9*82/+
X==(8/(2+2)+8/4-6/1
–>X==822+/84/+61/-
人工转化检验是否正确的标准主要有以下几点:
各个符号和数字的种类、数量是否和中缀表达式一致;
后缀表达式的最后一个字符必须是操作符,且此操作符必须是这个最终整体的操作符。 -
机器转化
机器转化时,需要使用栈这一数据结构,而且需要告诉计算机各个操作符的优先级,下列表中说明了常见的操作符的优先级:
所谓栈内优先级,就是指这个操作符在栈里面时,它的优先级;栈外优先级,就是指这个操作符在栈外面时,它的优先级。可以看到,除‘(’外,大多数操作符入栈之后,它的优先级都变高了。机器转化时对应算法的流程图如下:
机器转化的大致思想如下:
对于每一个字符,设置不同的优先级。对于数组字符,直接打印输出,碰到操作符时,比较一下栈顶元素与此操作符的优先级大小,如果栈内的字符优先级大,则栈内字符打印并出栈,否则此操作符入栈,直到扫描完整个表达式字符串为止。
中缀表达式转化为后缀表达式的C++源码如下:
suffix.cpp
#include <iostream>
#include <stack>
using namespace std;
class op{
public:
char c;
int priority;
};
int main(int argc, char **argv){
int i = 0;
stack<op> sop;
string express;
cin >> express;
op opString[express.length()];
op cop;
for(int i=0; i<express.length(); i++){
opString[i].c = express[i];
if(express[i]>='0' && express[i]<='9')
opString[i].priority = -1;
else{
if(express[i] == '#')
opString[i].priority = 0;
if(express[i] == ')')
opString[i].priority = 1;
if(express[i] == '+' || express[i] == '-')
opString[i].priority = 2;
if(express[i] == '*' || express[i] == '/' || express[i] == '%')
opString[i].priority = 4;
if(express[i] == '^')
opString[i].priority = 6;
if(express[i] == '(')
opString[i].priority = 8;
}
}
cop.c = '#';
cop.priority = 0;
sop.push(cop);
while(i < express.length()){
if(opString[i].priority < 0){
cout << opString[i].c;
i++;
}
else{
if(sop.top().priority > opString[i].priority){
if(sop.top().c != '(' && sop.top().c != ')')
cout << sop.top().c;
sop.pop();
}
else{
if(opString[i].c == '(')
opString[i].priority = 1;
if(opString[i].c == '+' || opString[i].c == '-')
opString[i].priority = 3;
if(opString[i].c == '*' || opString[i].c == '/' || opString[i].c == '%')
opString[i].priority = 5;
if(opString[i].c == '^')
opString[i].priority = 7;
if(opString[i].c == ')')
opString[i].priority = 8;
sop.push(opString[i]);
i++;
}
}
}
while(sop.top().c != '#'){
if(sop.top().c != '(' && sop.top().c != ')')
cout << sop.top().c;
sop.pop();
}
return 0;
}
// 2+4*4-9/(3-4) ----> 244*+934-/-
后缀表达式求值
到后缀表达式之后,求表达式的值就很简单了,机器通过后缀表达式求值的流程图如下:
对于后缀表达式的求值,步骤比表达式的转化要简单,其主要的算法就是依次扫描后缀表达式字符串中的每一个元素,见到数字字符,就先将其入栈,当碰到操作符时,进行相关的运算,并将运算结果入栈…依次操作,直到扫描完后缀表达式。
后缀表达式求值的C++源码如下:
express.cpp
#include <iostream>
#include <string>
#include <stack>
using namespace std;
int main(int argc, char **argv){
int result = 0;
int num1 = 0;
int num2 = 0;
string express = "244*+936-/-";
stack<int> cs;
for(int i=0; i<express.length(); i++){
if(express[i] >= '0' && express[i] <= '9')
cs.push(express[i] - '0');
else{
num1 = cs.top();
cs.pop();
num2 = cs.top();
cs.pop();
if(express[i] == '+')
result = num1 + num2;
if(express[i] == '*')
result = num1 * num2;
if(express[i] == '-')
result = num2 - num1;
if(express[i] == '/' ){
if(num1)
result = num2 / num1;
else{
cout << "Error: 0 after operator '/'" << endl;
return 0;
}
}
cs.push(result);
}
}
cout << "2+4*4-9/(3-6)" << " == " << result << endl;
return 0;
}
/* infix to suffix */
// 2+4*4-9/(3-6) ----> 244*+936-/-
源码里面针对’/'运算符,增加了一个if条件判断,主要的目的是当除数为零时,停止一切运算,并打印错误信息。
小结
本人在之前,已经用Java写出了一个Windows桌面计算器小程序,当时由于知识有限,没能将此博客中的原理运用在之前的Java程序里面。读者若是已经读懂本人此篇博客的基本原理,可以试着改写本人的Java程序,使其更加完善。
上面的两个程序里,本人仅仅只演示了表达式求值的原理,为了简化问题,均考虑的是操作数为一位十进制数的情况。而现实的所有计算器应用程序中都是多位十进制数运算的情形,此方面有待做进一步修改。虽然算数表达式求值的原理是简单的,但在现实开发里面运用起来,却并不那么容易!