Time Limit: 1sec Memory Limit:256MB
Description
请实现一个只包含加减乘除以及括号运算的简单计算器,忽略式子中的所有空格,输入格式是正确的,不需判断。
Input
第一行只有一个整数m,代表有m个测试用例
接下来有m行,每一行是一个测试用例,代表要求计算的式子,请注意输入的数字可能有小数。
Output
对于每个测试用例,输出一行,每一行只有一个数,表示式子的运算结果,保留3位小数(四舍五入)
Sample Input
2 1+2 1/3
Sample Output
3.000 0.333
_________________________________________________________________________________________________________
略有难度,略有难度。
这道题的描述计算器和电脑自带的计算器有区别,电脑的不接收整一串表达式,这道题则需要。
这种表达式是人眼能看懂的,行话叫“中缀表达式”,而计算机能看得懂的是“后缀表达式”(又叫“逆波兰式”)。比如,中缀表达式(a+b)*c-(a+b)/e的逆波兰式是ab+c*ab+e/-
怎么由中缀表达式转成后缀表达式呢?手动操作其实不难,把中缀表达式画成一棵表达式二叉树(叶节点是操作数,内部节点是操作符,优先级越高的操作符在越深层)即可。而把这棵二叉树按后根(post-order)遍历,就恰好能得到它的后缀表达式。
比如中缀表达式(a+b)*c-(a+b)/e,画出二叉树,后根遍历得到逆波兰式ab+c*ab+e/-
等一下,为什么我们需要后缀表达式?
因为有了后缀表达式就简单了。引用维基的例子:
中缀表达式“5 + ((1 + 2) * 4) − 3”写作 5 1 2 + 4 * + 3 −
下表给出了该逆波兰表达式从左至右求值的过程,堆栈栏给出了中间值,用于跟踪算法。
输入 | 操作 | 堆栈 | 注释 |
---|---|---|---|
5 | 入栈 | 5 | |
1 | 入栈 | 5, 1 | |
2 | 入栈 | 5, 1, 2 | |
+ | 加法运算 | 5, 3 | (1, 2)出栈;将结果(3)入栈 |
4 | 入栈 | 5, 3, 4 | |
* | 乘法运算 | 5, 12 | (3, 4)出栈;将结果(12)入栈 |
+ | 加法运算 | 17 | (5, 12)出栈;将结果 (17)入栈 |
3 | 入栈 | 17, 3 | |
− | 减法运算 | 14 | (17, 3)出栈;将结果(14)入栈 |
计算完成时,栈内只有一个操作数,这就是表达式的结果:14
接下来就是写代码了。难点在于将中缀表达式转换成后缀表达式。这位大神的文章很有用!
以下是我的代码,自认为注释比较详细:
#include <stdio.h>
#include <iostream>
#include <sstream>
#include <stack>
using namespace std;
// 枚举类型:标记操作符和操作数
enum Attribute { OPERATOR, OPERAND };
// 因为栈中要操作数、操作符混用,所以使用结构体,并用一个变量来标记是操作数还是操作符
struct node {
double value; // 操作数还是操作符的值
Attribute attr; // 标记是操作数还是操作符
node() {}
node(double v, Attribute a) : value(v), attr(a) {}
};
// 对原始表达式进行预处理
void initialize(string str, string &exp);
// 判断是否为操作符
bool isOperator(char a);
// 将人能读的算数表达式转换成逆波兰式(中缀表达式==>后缀表达式),生成栈result(栈顶是逆波兰式首部)
void buildPolish(string exp, stack<node>& result);
// 辅助函数,从calculation栈中取出2个操作数,进行相应的计算并返回结果
void calculate(stack<double>& calculation, char sign);
// 将逆波兰式计算出最终结果
double calPolish(stack<node> polish);
int main() {
int T; // 测试样例数
string str, exp; // str是原始表达式,exp则是经过预处理后得到的表达式
scanf("%d", &T);
getline(cin, str); // 若不这样先写一句getline,那么第一个getline结果是空字符串
while (T--) {
getline(cin, str); // 输入原始表达式
// 对原始表达式进行预处理
initialize(str, exp);
// 用于顺序存储逆波兰式的栈
stack<node> polish;
// 算数表达式转换成逆波兰式
buildPolish(exp, polish);
// 计算逆波兰式的算数结果
double answer = calPolish(polish);
printf("%.3lf\n", answer);
}
return 0;
}
// 对原始表达式进行预处理
void initialize(string str, string &exp) {
exp = "";
// 去除原表达式中的空格
// 同时,如果左括号的右边是'+'或'-',则在二者之间加入数字0,
// 从而强制地将'+'或'-'从操作数的正或负号变为加或减运算符
for (int i = 0; i < str.length(); i++) {
if (str[i] != ' ') {
exp += str[i];
}
if (str[i] == '(' && (str[i + 1] == '+' || str[i + 1] == '-')) {
exp += '0';
}
}
// 若表达式以'+'或'-'开头,则在开头处加入数字0
// 同样要强制地将'+'或'-'从操作数的正或负号变为加或减运算符
if (exp[0] == '+' || exp[0] == '-')
exp = "0" + exp;
}
// 判断是否为操作符
bool isOperator(char a) {
return a == '(' || a == ')' || a == '+' || a == '-' || a == '*' || a == '/';
}
// 将人能读的算数表达式转换成逆波兰式(中缀表达式==>后缀表达式),生成栈result(栈顶是逆波兰式首部)
void buildPolish(string exp, stack<node>& result) {
stack<node> s1; // 临时存储运算符(含一个结束符号#),越往栈顶符号优先级越高
stack<node> s2; // 用于输入逆波兰式
istringstream stream(exp); // 用于读操作数和操作符
double num; // 读入的操作数
char ch; // 读入的操作符
// s1中先放入结束符#
s1.push(node('#', OPERATOR));
for (int i = 0; i < exp.size(); i++) {
if (isOperator(exp[i])) { // 发现是操作符
stream >> ch; // stream向前走一位
// 遇'('则直接入栈s1
if (exp[i] == '(') {
s1.push(node(exp[i], OPERATOR));
}
// 遇')'则将距离栈s1栈顶的最近的'('之间的运算符,逐个出栈,并依次送入栈s2。最后s1抛弃'('
else if (exp[i] == ')') {
while (s1.top().value != '(') {
s2.push(s1.top());
s1.pop();
}
s1.pop();
}
// 遇下列运算符,则分情况讨论:
// 1.若当前栈s1的栈顶元素是'(',则当前运算符直接压入栈s1;
// 2.否则,将当前运算符与栈s1的栈顶元素比较:若优先级较栈顶元素大,则直接压入栈s1中;
// 否则将s1栈顶元素弹出,并压入栈s2中,
// 直到栈顶运算符的优先级别严格低于当前运算符,然后再将当前运算符压入栈s1中
else if (exp[i] == '+' || exp[i] == '-') {
char c = s1.top().value;
while (c != '#') {
if (c == '(') {
break;
}
else {
s2.push(s1.top());
s1.pop();
}
c = s1.top().value;
}
s1.push(node(exp[i], OPERATOR));
}
else if (exp[i] == '*' || exp[i] == '/') {
char c = s1.top().value;
while (c != '#' && c != '+' && c != '-') {
if (c == '(') {
break;
}
else {
s2.push(s1.top());
s1.pop();
}
c = s1.top().value;
}
s1.push(node(exp[i], OPERATOR));
}
}
// 发现是操作数
else {
stream >> num; // 用stream读出
s2.push(node(num, OPERAND)); // 直接压入s2
// 表达式字符串的下标i连续前进若干位直到碰见操作符
// 【注】无法处理形如1e-4的用科学计数法表示的数,
// 因为本解法中'+'或'-'都被当做加减号处理,而非数字的一部分
while (!isOperator(exp[i + 1]) && exp[i + 1] != '\0') {
i++;
}
}
}
// 若栈s1非空,则将栈中元素依次弹出并压入栈s2中
while (!s1.empty() && s1.top().value != '#') {
s2.push(s1.top());
s1.pop();
}
// 此时s2中已经是逆波兰式,只不过式子的首部在栈底。所以把s2翻转过来,用另外一个栈存储
while (!s2.empty()) {
result.push(s2.top());
s2.pop();
}
}
// 辅助函数,从calculation栈中取出2个操作数,进行相应的计算并返回结果
// 注意操作数顺序!先出栈的操作数是减数(除数),后出战的操作数是被减数(被除数)
void calculate(stack<double>& calculation, char sign) {
double op1 = calculation.top();
calculation.pop();
double op2 = calculation.top();
calculation.pop();
switch (sign) {
case '+':
calculation.push(op2 + op1); break;
case '-':
calculation.push(op2 - op1); break;
case '*':
calculation.push(op2 * op1); break;
case '/':
calculation.push(op2 / op1); break;
}
}
// 将逆波兰式计算出最终结果
double calPolish(stack<node> polish) {
// 用一个栈存储临时结果
stack<double> calculation;
while (!polish.empty()) {
// 如果逆波兰式的当前元素是操作数,则直接压进calculation栈
if (polish.top().attr == OPERAND) {
calculation.push(polish.top().value);
}
// 如果逆波兰式的当前元素是操作符,则从calculation栈中取出2个操作数,进行相应计算
// 之后把计算结果压进calculation栈
else if (polish.top().attr == OPERATOR){
calculate(calculation, char(polish.top().value));
}
polish.pop();
}
// 最后,calculation栈顶的元素一定是最终结果
return calculation.top();
}