1. 前言
经常会遇到这样一种类型的编程题目,给定一个合法算术运算式字符串(本文不考虑非法情况的判断和处理),求出它的计算结果,通常字符串中包含了 +-×/
四则运算、改变运算优先级的括号 ()
以及整数数字.
例如,要求程序输入 2×5+6×(3+2)
这样一个字符串,输出其正确的计算结果也就是 40
.
2. 表达式表示法
表示方法有 3 种,分别是前缀表达式(波兰表达式)、中缀表达式、后缀表达式(逆波兰表达式),以之前的例子 2×5+6×(3+2)
为例.
- 前缀表达式:将操作符置于操作数的前面,
+ 1 2
表示“一加二”.2×5+6×(3+2)
对应的前缀表达式是+ × 2 5 × 6 + 3 2
,简单的算术运算符都是二元的,所以前缀表达式无需括号,也无歧义. - 中缀表达式:最常见的形式,符合人们的阅读方式,
2×5+6×(3+2)
本身就是中缀表达式. - 后缀表达式:将操作符置于操作数的后面,
1 2 +
表示“一加二”.2×5+6×(3+2)
对应的后缀表达式是2 5 × 6 3 2 + × +
,同样没有括号,这种表示方法非常便于计算机进行处理.
3. 字符串预处理
遍历原始字符串,分别将每个运算符和操作数都作为一个 string 类型的变量存储起来,最终得到一个 string 数组作为原始中缀表达式,这个过程中涉及到:
- 删除原始字符串的多余空格.
- 多字符数字的提取.
-
作为负数标志而不是运算符.
//解析原始中坠表达式,原始字符串中有冗余空格,同时包含"+,-,*,/,(,)"和十进制数
vector<string> prepare(string raw){
vector<string> infix;//按顺序返回操作数和运算符的列表
int len=raw.length();
for(int i=0;i<len;){
if(' '==raw[i]){ ++i;continue; }
if('0'<=raw[i] && raw[i]<='9'){//解析数字
int j=i;
while(j<len && '0'<=raw[j] && raw[j]<='9') ++j;
infix.push_back(raw.substr(i,j-i));
i=j;
}
else if('-'!=raw[i]){//解析运算符
infix.push_back(raw.substr(i,1));
++i;
}
else{//对'-'特殊处理
if(i>0 && (('0'<=raw[i-1] && raw[i-1]<='9') || ')'==raw[i-1])){
infix.push_back(raw.substr(i,1));
++i;
}
else{
int j=i+1;
while(j<len && '0'<=raw[j] && raw[j]<='9') ++j;
infix.push_back(raw.substr(i,j-i));
i=j;
}
}
}
return infix;
}
4. 运用表达式树计算
4.1 表达式树
二叉树是表达式处理的常用工具,2×5+6×(3+2)
对应的表达式树如下所示,所有的操作数都在叶结点上,所有的运算符都在非叶结点上.
表达式树的前序遍历序列和后序遍历序列分别对应着前缀表达式和后缀表达式,而中序遍历序列对应着去掉括号以后的中缀表达式.
4.2 由中缀表达式建立二叉树
找到当前表达式进行“最后计算”的运算符(它是整个表达式树的根),然后递归处理左右两个规模更小的表达式.
如何去寻找最后才进行计算的运算符?可以从左向右遍历当前表达式(因为表达式是从左向右进行计算的,最右边的式子最后才进行计算),统计括号外的 +-
符号和 ×/
符号最后出现的位置,由于乘除法优先计算,所以优先将括号外的 +-
符号作为最后的运算符,括号之外不存在 +-
符号时才将括号外的 ×/
符号作为最后的运算符. 如果 +-×/
都不存在,说明当前整个表达式被左右两个括号包含,需要去掉左右两个括号,继续递归处理.