• 如何计算一个字符串表示的计算式的值?——C_递归算法实现


    《C程序设计伴侣》的8.7.3 向main()函数传递数据这一小节中,我们介绍了如何通过main()函数的参数,向程序传递两个数据并计算其和值的简单加法计算器add.exe。这个程序,好用是好用,就是太简单,还停留在幼儿园大班的水平,只能计算两位数的加法。我们现在基本都已经是大学生了,如果还是用这个简陋的加法计算器去向面试官展示我们的编程能力,肯定会遭到他们的笑话。

    在看完《C程序设计伴侣》后,我们的编程能力已经今非昔比了。自然,我们也可以利用从这本书中学到的知识(函数,字符串处理等),把这个计算器改进一下,让他成为一个可以计算更多数据更多算符的高级计算器。

    我们是怎么计算一个复杂计算式的?我们总是根据要求列出一个计算式,这个计算式中有数字(整数)和对数字进行操作的算符(+,-,*,/四种运算),然后,从左到右依次计算,最后得到结果。比如,我们要计算

    1 + 2 – 3 * 4

    我们总是先计算3*4得到12,然后计算1+2得到3,最后计算3-12得到结果-9;而如果我们想用程序对这个字符串表示的计算式进行计算,又该如何进行呢?如果这个计算式比较简单,比如,只有1+2,我们倒是可以找出其中的符号和数字字符,然后将数字字符转换为数字进行计算,而如果这个计算式比较复杂,比如这里的1+2-3*4又该如何进行呢?

    回想一下,在数学课上老师是怎么教我们的?面对复杂的计算式,我们可以把它拆分成多个不太复杂的计算式,而不太复杂的计算式我们又可以将它拆分成简单的计算式。这种“大事化小,小事化了”的解题思路,刚好切合了我们的递归函数的设计思路。换句话说,他们都是将一个大问题转化为同类型的小问题,逐渐分解,直到最后可以很容易的得到结果。按照这样的递归函数的设计思路,同时结合数学中计算式的结合律(为了符合结合律,我们查找字符串中的运算符时,从字符串的末尾find_last()开始查找,这样可以避免运算顺序改变后更改运算符号。比如,6-3-3,如果我们从字符串的开始查找运算符,首先找到第一个减号,计算式被分为了(6) – (3-3)两部分,这样计算的结果就不正确了,如果我们从末尾开始查找运算符,则分解后得到(6-3) – (3)这样计算结果就是正确的。)另外还需要注意的是,乘除运算的优先级是高于加减运算的,因为函数的递归,实际上是最先计算最里层的函数,所以,我们应该先分解加减运算,将乘除运算放到最里层。

    按照上面的思路分析,我们可以把这个更高级的,可以计算计算式字符串的计算器实现如下:

    /*
     * eval.c
     *
     *  Created on: 2013年11月1日15:21:51
     *  Author: Bruce
     */
    #include <string.h>
    #include <stdio.h>
    
    int find_last(const char* s,char a)
    {
        int pos = strlen(s);
        //从字符串末尾位置开始查找
        const char* p = s + pos;
        //如果没有到达字符串开始的前一个位置(s-1)
        while((s-1) != p)
        {
            //如果蛋清位置的字符就是要查找的字符
            if(*p == a)
            {
                break;  //结束查找
            }
            p--;  //变换到下一个位置
            pos--;
        }
        if((s-1) != p)  //找到字符
        {
            return pos;
        }
        else  //未找到
        {
            return -1;
        }
    }
    //取得字符串的左半部分
    char* left_str(char* s, int pos)
    {
        s[pos]  = '';
        return s;
    }
    //取得字符串的右半部分
    char* right_str(char* s, int pos)
    {
        return s + pos + 1;
    }
    //计算字符串计算式s的值
    int eval(char* s)
    {
        int n = 0;
        //找到最后一个加号
        n = find_last(s,'+');
        if(-1 != n)
        {
            return eval(left_str(s,n)) + eval(right_str(s,n));
        }
        n = find_last(s,'-');
        if(-1 != n)
        {
            return eval(left_str(s,n)) - eval(right_str(s,n));
        }
        n = find_last(s,'*');
        if(-1 != n)
        {
            return eval(left_str(s,n)) * eval(right_str(s,n));
        }
        n = find_last(s,'/');
        if(-1 != n)
        {
            return eval(left_str(s,n)) / eval(right_str(s,n));
        }
        //当字符串中不包含运算符时,返回这个数字本身
        return atoi(s);
    }
    int main(int argc,char* argv[])
    {
        //检查参数是否合法
        if(2 != argc)
        {
            puts("usage: eval 1+2+3");
            return 1;
        }
         // 复制从参数得到的计算式字符串
        char expr[32] = "";
        strcpy(expr,argv[1]);
    
          // 对计算式字符串进行计算,得到结果
        int res = eval(expr);
        printf("%s = %d",argv[1],res);
        return 0;
    }

    现在,我们就可以用这个更高级的计算器计算最开始的那个计算式了:

    F:code>gcc -o eval.exe 35.c

    F:code>eval 1+2-3*4
    1+2-3*4 = -9

    我们使用递归函数的方法,计算了一个简单字符串计算式的值。这种方法简单是简单,可是却有一个漏洞,那就是他无法计算带有括号的,改变了运算顺序的计算式。比如,他无法计算

    1+(2-3)*4

    这个简单表达式的值。如果遇到了计算式中有括号(这是很常见的),又该如何计算呢?

    这个问题,实际上是编译原理中经典的一个问题,只要是计算机专业的同学,在学习编译原理的时候,几乎都会遇到,在网络上搜索一下,发现这实际上就是这门课程的一个作业题目,还有同学在网上问如何如何解决这道题目呢。

    【问题描述】 设计一个实现表达式求值的演示程序。 【基本要求】 当用户输入一个合法的算术表达式后,能够返回正确的结果。能够计算的运算符包括:加、减、乘、除、括号;
    能够计算的操作数要求在实数范围内;对于异常表达式能给出错误提示。
    【测试数据】 (1)请输入您所求的表达式 3*(7-2)+5 多项式的结果是: 20 (2)请输入您所求的表达式 3.154*(12+18)-23 多项式的结果是: 71.62 【实现提示】 1首先置操作数栈为空栈,表达式起始符#为运算符栈的栈底元素; 2依次扫描表达式中每个字符,若是操作数则进OPND栈;若是运算符,
    则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕。 3先做一个适合个位的+-*/运算, 其次就要考虑到对n位和小数点的运算。

          这应该算是编译原理中最常见的一个题目了。所谓求人不如求己。只要我们掌握了编译原理的基础原理,掌握了C++相关的基本知识(特别是STL中stack容器的使用(参见《我的第一本C++书》 ),如果只是学了C语言(参考《C程序设计伴侣》 ),需要知道栈的基本操作,可以自己实现一个栈),在按照这里的实现提示,就可以很轻松地自己解决这个问题。

    你可以先尝试自己解决这个问题,也可以参考下面的实现,整个算法的思路在注视中。

    /*
     * eval2.cpp
     *
     *  Created on: 2013年11月2日13:39:31
     *      Author: Bruce
     */
    
    #include <iostream>
    #include <string>
    #include <cctype>
    #include <stack>
    using namespace std;
    
    //返回两个 操作符之间的优先级关系
    char cmp(char a,char b)
    {
        switch(a)
        {
            case '#': //'#'优先级最低
                return ('#' == b)? '=' : '<';
                break;
    
            case '-':
            case '+': //'+' '-'的优先级小于'*' '/' '('
            {
                if('*' == b || '/' == b || '(' == b)
                {
                    return '<';
                }
                else
                {
                    return '>';
                }
            }
            break;
    
            case '*':
            case '/': // '*''/'的优先级小于'('而大于其他
            {
                return ('('==b)?'<':'>';
            }
            break;
    
            case '(': // '('的优先级等于')'
                return (')'==b)?'=':'<';
                break;
            default: // 不支持的操作符,抛出异常
                throw "error:unkown operator";
        }
    }
    
    // 用操作符对两个操作数进行操作,返回结果
    int calc(int a, int b, char op)
    {
        switch(op)
        {
            case '+':
                return a+b;
            case '-':
                return a-b;
            case '*':
                return a*b;
            case '/':
                if(0 == b) // 特殊处理除数为0
                    throw "error: the divisor shoud not be negtive.";
                else
                    return a/b;
            default:
                throw "error: unknown operator.";
        }
    }
    
    // 判断当前字符是否是操作符
    bool isoptr(char c)
    {
        // 合法的操作符列表
        static string optrs("+-*/()#");
        // 如果在列表中无法找到
        if(optrs.find(c) == string::npos)
        {
            if(isdigit(c))
                return false;    //不是算符
            else // 不支持的操作符
                throw "error: unknown char.";
        }
        return true; // 是操作符
    }
    
    //求计算式e的值
    int eval(string e)
    {
        e += "#"; //添加一个#表示表达式结束
        stack<int> opnd; //操作数栈
        stack<char> optr; //操作符栈
        optr.push('#'); //在操作符栈添加'#'表示开始
        int i = 0;  //计算式的起始扫描位置
        int num = 0; //从表达式中提取数字
    
        //这个字符解析计算式
        //直到表达式没有遇到结束符'#'
        //或者操作符栈中还有操作符
        while(e[i] != '#' || optr.top() != '#')
        {
            //判断当前字符是否是操作符
            if(!isoptr(e[i]))
            {
                 // 不是操作符,则是操作数
                // 利用循环从计算式中提取数字
                num = 0;
                // 逐个字符向后遍历,直到遇到操作符为止
                while(!isoptr(e[i]))
                {
                    num *= 10; // 将已经提取的数字向前移动一位
                    num += e[i] - '0'; // 加上当前数字,
                    ++i;
                }
                //将操作数压入操作数栈
                opnd.push(num);
            }
            else  // 如果当前字符是操作符
            {
                // 比较当前操作符与操作符栈顶操作符的优先级
                // 根据优先级采取不同策略
                if(optr.empty())
                {
                    throw "error: optr is empty";
                }
                switch(cmp(optr.top(),e[i]))
                {
                    // 栈顶操作符优先级低,暂不计算,新操作符入栈
                    // 比如在1+2中,操作符栈中最开始的#和+比较,
                    // #小于+,所以不执行计算,+直接压入操作符栈
                    case '<': // 小于
                        optr.push(e[i]);
                        ++i; // 解析下一个字符
                        break;
    
                    // 优先级相等,说明')'遇到了'(',
                    // 或者是'#'遇到了'#',
                    // 那么')'或'#'出栈,新符号不入栈
                     case '=':
                         optr.pop();
                         ++i;
                         break;
    
                     // 栈顶运算符优先级高,暂停输入,计算
                     // 比如,1+2#末尾的#,当他与此时栈顶+比较
                     // +的优先级大于#,从操作数栈中取两个数1和2,
                     // 同时取出操作符栈顶的+进行计算
                     case '>':
                         // 取出两个数计算结果
                         if(opnd.empty())
                         {
                             throw "error: opnd is empty.";
                         }
                         // 从操作数栈中取第一个数
                         int a = opnd.top(); opnd.pop();
                         if(opnd.empty())
                         {
                             throw "error: opnd is empty.";
                         }
                         // 取第二个数
                         int b = opnd.top(); opnd.pop();
    
                         // 这里要注意a,b的顺序,a先出栈,也就是后入栈,说明是操作符
                         // 之后的操作数,所以这里应该是b op a
                         // 将计算结果压入操作数栈,作为新的操作数
                         opnd.push(calc(b, a, optr.top()));    //注意这里a和b的顺序
                         // 已经计算过的操作符出栈
                         optr.pop();
                         // 注意,这里没有进行++i,
                         // 而是直接再次对当前操作符进行处理
                         break;
                }
            }
        }
        return opnd.top();
    }
    
    int main()
    {
        string expr;    // 计算式
        while(true)
        {
            cout<<"please input the expression.  'end' for exit"<<endl;
            cin>>expr;
             if("end" == expr)
                 break;
             try
             {
                 int res = eval(expr);
                 cout<<expr<<" = "<<res<<endl;
             }
             catch (const char* err)
             {
                 cout<<err<<endl;
             }
        }
        return 0;
    }

    现在,这个计算器已经足够高级了,他可以计算加减乘除和括号,也能够对异常情况进行处理。比如一开始的那个计算式:

    F:code>eval
    please input the expression.  ‘end’ for exit

    1+(2-3)*4

    1+(2-3)*4 = -3

    please input the expression.  ‘end’ for exit

    end

    唯一的遗憾是目前他只支持整数,而题目的要求是实数范围内,不过不要紧,只要我们看明白了整个算法的思路和过程,自然可以轻松将其扩展到实数范围。即使是老师险恶地要求扩展到支持其他运算,比如乘方开方等,我们自己也能搞定,再也不用到处求爷爷告奶奶了。

    这个例子也再次证明了毛主席的那句话:

    只有自己动手,才能丰衣足食!

    本文在创作过程中参考了以下两篇文章,特此鸣谢。

    转自:http://www.howzhi.com/course/3387/lesson/43244

  • 相关阅读:
    【cocos2d-js官方文档】二、资源管理器Assets Manager
    【cocos2d-js官方文档】七、CCFileUtils
    【cocos2d-js官方文档】九、cc.loader
    【cocos2d-js官方文档】十二、对象缓冲池
    【cocos2d-js官方文档】十二、对象缓冲池
    【cocos2d-js官方文档】二十一、v3相对于v2版本的api变动
    【cocos2d-js教程】cocos2d-js 遮挡层(禁止触摸事件传递层)
    cocos2d-js中怎么删除一个精灵
    MS Chart 折线图——去除时间中的时、分、秒,按天统计【转】
    MS Chart 条状图【转】
  • 原文地址:https://www.cnblogs.com/kingshow123/p/3402302.html
Copyright © 2020-2023  润新知