题目链接:
题目描述:
简单计算器
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 16398 Accepted Submission(s): 5628
1 + 2 4 + 2 * 5 - 7 / 11 0
3.00 13.36
解析:
这道题典型的用栈模拟的习题,我们可以建立两个栈,一个用来存放字符,另外一个用来存放数字,一开始的时候,两个栈都是空的,然后我们将输入的字符串从头到尾一直遍历过去,数字和运算符不断入栈,当检测到当前遍历到的运算符比栈顶的运算符优先级更低时,或者当前遍历到的运算符比栈顶的运算符优先级同为高级(*或/),栈顶那么说明此时栈顶的运算符需要计算了,所以从数字栈中取出两个数进行运算,运算后将结果再压入数字栈中,并且将运算符栈的栈顶元素作出栈处理。注意:从数字栈中取出两个数,如果是作除法运算,应该返回值是b/a,减法运算也一样,返回值是b - a;原因是b比a先入栈,说明b在表达式的前面,而减法运算和除法运算是不满足交换律的。然后重复上述运算,直到遍历完毕。
遍历结束之后,那么就结束运算了吗?很明显是没有的,比如说计算:
1 * 2 - 4 + 5;
由于运算符优先级的问题,那么这个表达式一开始遍历到第二个运算符‘-’号时,发现栈顶‘*’运算符的优先级更高,所以取出两个数进行乘法运算,然后将结果压入数字栈,而后再往后遍历,直到遍历结束之后,都没有再进行运算。那么此时
数字栈中从栈底到栈顶依次有2,4,5;
运算符栈从栈底到栈顶依次有-,+;
而对应的表达式是2-4+5(很明显我们口算得结果为3).
那么在for循环遍历结束后,我们要判断一下运算符栈是否为空,不为空则继续运算。
这时候,你可能会想,继续按照上述思路计算不就行了,但是事实真的如此吗?
我们可以试着算一算,第一步是计算4+5 = 9,然后9入栈(此时数字栈中剩下2和9,运算符栈中剩下‘-’号运算符),然后再是第二步,计算2 - 9 = -7,然后判断得运算符栈为空,那么此时输出数字栈栈顶元素即为答案,而这样做的答案却是-7,很明显是错误答案,我们分析一下答案错在了哪里。
首先,综合一下计算得过程,算得-7的答案的表达式为 2-(4+5) = -7,而我们实际表达式是2-4+5,造成上述错误答案的原因就是运算符优先级出现了问题,2-(4+5) 是先计算了加法,再计算减法,相当于把整个表达式逆序运算了。而2-4+5这个表达式中只含有‘-’和‘+’,同级运算符应当从左向右依次运算,即要把原来逆序运算的错误形式改正。
那么很自然我们就想到了把数字栈和运算符栈颠倒一下即可:
颠倒前:
数字栈中从栈底到栈顶依次有2,4,5;
运算符栈从栈底到栈顶依次有-,+;
颠倒后:数字栈中从栈底到栈顶依次有5,4,2;
运算符栈从栈底到栈顶依次有+,-;
而颠倒之后,再进行逆序运算就相当于原来的栈进行正序运算。(需要注意的是,在颠倒前,原来‘-’号两边的数是b先入栈,a后入栈,所以返回结果是b-a,而颠倒后,b到了a的后面,即颠倒后a更靠近栈底,如果按照写好的运算函数的话,返回值将是a-b,但是正确结果是b-a,所以应该对返回值取相反数)
计算过程:(1)-(4 - 2) = -2
(2)5+(-2) = 3
结果正确,然后我就信心满满的交了,却得到了这样的答案:
于是我就纳闷了,都分析这么透彻了,怎么还是wrong answer?
然后我就开始测试大量的数据,终于发现了一个错误,比如说如下的等式:
1 + 2 - 4 / 8
我们口算得答案为2.50;
如果按照上述规则运算,则在for循环遍历时不会进行任何运算,然后在循环体外先将栈颠倒,得到此时的数字栈和运算符栈为:
数字栈中从栈底到栈顶依次有8,4,2,1;
运算符栈从栈底到栈顶依次有/,-,+;
那么运算过程为:8/4 = 2;-(2-2) = 0;1+0 = 1
很明显,得出了错误答案,我们也得到了错误原因,那就是除法运算也"逆"运算了,而将“逆”运算转“正”是针对同级运算符,在这里我们要先计算更高级的运算符才能翻转栈。根据运算规则:
当检测到当前遍历到的运算符比栈顶的运算符优先级更低时,或者当前遍历到的运算符比栈顶的运算符优先级同为高级(*或/),栈顶那么说明此时栈顶的运算符需要计算了,所以从数字栈中取出两个数进行运算,运算后将结果再压入数字栈中,并且将运算符栈的栈顶元素作出栈处理。
经过分析后我们能够知道,运算符栈中最多存在一个‘*’或者‘/’在遍历中未进行运算,而且一定是位于栈顶。
而有了这样的分析之后,我们在翻转整个栈之前的话,可以先进行判断一下,栈顶是否为'*'或者‘/’,如果是的话,则可以先进行运算,然后再翻转整个栈即可。
经过简单的修改之后,得到了这样的答案:
完整代码实现:
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
using namespace std;
template <class Type> class Stack{
private:
Type *data;
int top_index,Maxsize;
public:
Stack(int m_size){ //构造函数
data = new Type[m_size];
top_index = -1;
Maxsize = m_size;
}
~Stack(){ //析构函数
delete [] data;
}
void push(Type element){
if(top_index >= Maxsize-1){ //栈满,插入失败
return;
}
++top_index;
data[top_index] = element;
}
void pop(){
if(top_index < 0){ //栈空,出栈失败
return;
}
--top_index;
}
Type top(){ //获取栈顶元素
if(top_index > -1){
return data[top_index];
}
}
bool empty(){
return top_index == -1;
}
int cnt_num(){
return top_index + 1;
}
void reverse_stack(Stack <Type> &_stack){
Type temp[210];
int i = 0;
while(!_stack.empty()){
temp[i] = _stack.top();
_stack.pop(); //栈顶元素出栈
i++;
}
for(int j = 0;j < i;j++){
_stack.push(temp[j]);
}
}
};
bool judge_priority(char a,char b){ //判断是否优先级更高
if((a=='*'&&b=='+')||(a=='*'&&b=='-')||(a=='/'&&b=='+')||(a=='/'&&b=='-')){
return true;
}
return false;
}
bool judge_equal(char a,char b){ //判断两个运算符优先级是否相等
if((a=='+'&&b=='+')||(a=='-'&&b=='-')||(a=='+'&&b=='-')||(a=='-'&&b=='+')){
return true;
}
return false;
}
bool priority(char a,char b){
if(judge_priority(a,b) || judge_equal(a,b)){
return true;
}
return false;
}
double calc(Stack <double> &numbers,Stack <char> &operations){
double a = numbers.top();
numbers.pop();
double b = numbers.top();
numbers.pop();
char c = operations.top();
operations.pop();
if(c=='+'){
return b+a;
}
if(c=='-'){
return b-a;
}
if(c=='*'){
return b*a;
}
if(c=='/'){
return b/a;
}
}
bool is_num(char cc){ //判断其是否为数字字符
if(cc >= '0' && cc <= '9'){
return true;
}
return false;
}
bool is_operation(char cc){ //判断其是否为'+','-','*','/'操作符
if(cc=='+'||cc=='-'||cc=='*'||cc=='/'){
return true;
}
return false;
}
int main()
{
char str[210]; //前者存放全部字符
while(cin.getline(str,210) &&strlen(str) > 1){
Stack <double> numbers(210);
Stack <char> operations(210);
for(int i = 0;str[i] != '\0';i++){
if(is_num(str[i])){
double ans = 0;
for(;str[i] != '\0';i++){
if(is_num(str[i])){
ans = ans * 10 + str[i] - '0';
}
else{
break;
}
}
numbers.push(ans);
}
else if(is_operation(str[i])){
if(operations.empty() || priority(str[i],operations.top())){
operations.push(str[i]);
}
else{
numbers.push(calc(numbers,operations));
i = i - 1; //回溯,继续执行下面的语句,则跳转至i++部分
}
}
}
bool tmp = false;
while(!operations.empty()){
if(operations.top()!='*'&& operations.top()!='/'&&!tmp)
{
operations.reverse_stack(operations);
numbers.reverse_stack(numbers);
tmp = true;
}
else if(operations.top()=='-'){
numbers.push(-1.0*calc(numbers,operations));
}
else{
numbers.push(calc(numbers,operations));
}
}
printf("%.2f\n",numbers.top());
memset(str,0,sizeof(str));
}
return 0;
}
需要注意的是:这道题用G++提交的时候竟然wrong answer,原因是我改变了cout输出流的输出形式,而用C++提交却Accept,然后我用printf("%.2f\n",numbers.top());时,用G++
提交才Accept。
附上错误原因,以后警示自己:(具体原因我也不太清楚,知道的可以私信我一下)
cout.setf(ios::fixed);
cout<<setprecision(2);
cout << numbers.top() << endl;
//用G++提交wrong answer
//以后还是尽量用printf输出,避免不必要的错误
但是,我们是否就满足于这样的代码? (代码150+行,而且易错点极多,思路也不清晰)
这样的代码是比较糟糕的,所以我们重新审视一下这道题:
从题干开始,我们是否一开始就要读入全部字符?
为什么题干的输入会隔着一个空格之后再接着运算符?
是否一定需要将整型数字也声明为字符型?
带着以上问题,我们可以先观察一下运算表达式:
4 + 2 * 5 - 7 / 11数字有5个,运算符有4个,当输入0的时候结束输入,那么其实我们可以换一种思路,输入多少读取多少,而不必一下子全部输入,这样的话带来不必要的麻烦。
一开始我们输入并读取第一个数字,然后再判断第一个数字以及紧接着的字符,如果是0和'\n'(回车是表示输入结束的标志),那么跳出循环体即可。然后再看剩下的部分,4个数字+4个运算符,这样的话,其实我们可以两两输入,再读取。
如果读取到‘*’的话,那么我们就可以进行乘法运算,并将结果保存在前一个数字中,除法同理。
而如果读取到‘+’的话,由于运算符优先级的问题,那么我们就需要把‘+’运算符一起读取的数字存储起来,存储在上一位读取到的数字并存储的下一位即可,而'-'号的话,为了最后运算简便,我们将减法运算统一转换成加法运算。(即5-2 = 5 + (-2))那么同样的,将读取到的数字存储其相反数至数组即可。
直到字符读取到‘\n’则表示读取结束。
然后将所有存储好的数相加即可得出答案。
例如上述表达式,运算过程为:
一开始输入num[0],并读取第一个数4和紧接着的字符,发现不满足0和'\n'的条件。因此将4存储后,即num[0] = 4,然后继续输入并读取。
第一步:输入 ‘+’ 和数字2,读取后,发现‘+’运算符,因此将2存储,即num[1] = 2;
第二步:输入 ‘*’ 和数字5,读取后,发现‘*’运算符,因此将与前一个存储的数进行乘法运算然后再存储至前一个数,即num[1] =num[1] * 5,即最后存储得num[1] = 10;
第三步:输入 ‘-’ 和数字7,读取后,发现‘-’运算符,因此将7取反后存储,即num[2] = -7;
第四步:输入 ‘/’ 和数字11,读取后,发现‘/’运算符,因此将与前一个存储的数进行除法运算然后再存储至前一个数,即num[2] =num[2] / 11,即最后存储得num[2] = -0.64;
至此,读取结束。
然后剩下了num[0],num[1],num[2]为各种运算符计算后的结果,再将所有结果相加后即得答案为13.36
完整代码实现:
#include<cstdio>
int main(){
double num[100],f;
while(scanf("%lf",&num[0])==1){
double ans = 0;
char s = getchar();
if(num[0]==0&&s=='\n') break;
int i = 0;
while(scanf("%c %lf",&s,&f)==2){
if(s=='*') num[i] *= f;
else if(s=='/') num[i] /= f;
else if(s=='+') num[++i] = f;
else num[++i] = -f;
if((s=getchar()=='\n')) break;
}
while(i>=0){
ans += num[i];
i--;
}
printf("%.2f\n",ans);
}
return 0;
}
总结:要善于挖掘题目信息,留意题干中比较奇怪的部分(比如说这题的数字和运算符之间隔着一个空格,这正是利用scanf输入数据的特点,两个两个输入再读取即可),然后做题目,一定要完完全全想清楚之后再写代码,这是一个惨痛的教训!(写了整整一个晚上,开会的时候想清楚了十几分钟就重新写出了完整代码),想清楚再写代码绝对会有事半功倍的效果,不要太心急,要沉稳!当一个bug超过半小时仍未找出来的时候,那么这时候果断把代码删除重写。
优化自己的代码也是一个好的程序员必备的素质。
学会写测试数据也是非常重要的一项技能。
附上该题自己制作的一些测试数据:
1 + 2 * 3
1 + 2 * 0
1 * 2 * 3
1 / 2 / 3
0 + 0
1 + 0
0 + 1
1 * 2 / 3 + 4 * 0
1 * 2 + 3
0 + 0
0 * 0
1 * 0 * 2 * 3
1 + 2 / 3 / 4
1 + 0 / 3 / 4
1 + 2 + 3 + 4
1 + 2 - 4 + 5 - 9
1 + 2 * 3 * 4
1 - 4 / 3
1 - 3 * 4
如有错误,还请指正,O(∩_∩)O谢谢