HDU1237(栈的简单应用以及巧解的方法)

题目链接:

简单计算器

题目描述:

简单计算器

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 16398    Accepted Submission(s): 5628


Problem Description
读入一个只包含 +, -, *, / 的非负整数计算表达式,计算该表达式的值。
 

Input
测试输入包含若干测试用例,每个测试用例占一行,每行不超过200个字符,整数和运算符之间用一个空格分隔。没有非法表达式。当一行中只有0时输入结束,相应的结果不要输出。
 

Output
对每个测试用例输出1行,即该表达式的值,精确到小数点后2位。
 

Sample Input
   
   
   
   
1 + 2 4 + 2 * 5 - 7 / 11 0
 

Sample Output
   
   
   
   
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谢谢


你可能感兴趣的:(栈,代码优化,数学问题,思维到位,读题细节问题)