04 Java API:arraylist实现算术表达式的解析

来源:互联网 发布:老虎证券开户 知乎 编辑:程序博客网 时间:2024/05/17 05:12

java API包罗万象,细节层出不穷,要在一篇笔记里总结好实在是很有挑战性。笔者思前想后,决定还是用毕老师视频中讲到的API知识实现一个小项目,并把思考和编码过程记录在下面,在这个过程中练习API的使用。这个项目是实现一个算术表达式解析器。网上有很多针对此项目的程序,不过大多要用到中缀表达式的转换算法和栈结构。笔者在查阅这些资料的时候感到因为不符合手算的经验,这些代码都难以理解。能不能用小学生算术课上的分步化简的办法来完成呢?也就是说解析器在运算过程中操作数和操作符不会变换相对位置,仅仅用计算、替换、删除来实现化简,最终表达式被化简为一个操作数,即答案。

笔者用两天时间完成了这个程序。用到的API主要有包装类,String类和ArrayList框架类。

1. 概述

算术表达式解析器能够按照算术运算的规则来计算一段以字符串形式表述的算术表达式的值。比如:
String expr = "1 + 2 * 4 - 3";
这个表达式的值应当是6。
考虑更复杂的表达式:
String expr = "( - 2)*(2 - 2*(3 + 4) + 9*2)";
计算结果是 - 12。

2. 需求

输入:
一个字符串,该字符串表示一个完整正确的算术表达式
该表达式可以包括括号,包括正负零三种范围的整数或小数操作数,包括加减乘除四种运算
输出:
该表达式的double类型计算结果
其他要求:不使用栈,不对操作符进行重新排序,仿照手算的顺序按照计算、替换、删除的方式不断化简直到得出答案,代码尽可能精简

3. 设计

3.1 选择一种框架类来存储表达式

项目需求中要求按照手算顺序不断化简得出答案。手算例子中的表达式可以计算如下:
( - 2)*(2 - 2*(3 + 4) + 9*2) = ( - 2)*(2 - 2*(7) + 9*2) = ( - 2)*(2 - 2*7 + 9*2) = ( - 2)*(2 - 14 + 9*2) = ( - 2)*(2 - 14 + 18) = ( - 2)*( - 12 + 18) = ( - 2)*(6) = ( - 2)*6 =  - 12
我们要设计的程序每一步消去一个操作符。因为操作数的个数始终比操作符多一个,到最后会剩下一个操作数就是答案。
要模仿这个分步计算的过程,这个框架类作为表达式的容器,应当满足:
为了方便定位操作符和操作数,能够按索引查找元素
为了方便替换和删除操作符和操作数,能够在遍历中按索引更改删除元素
为了删除功能,容器的元素总数可以变化
按索引查找,可以用List类的容器实现。按索引改删,可以用ArrayList。

3.2 决定框架类中存储什么类型的元素

ArrayList中存储的元素是什么呢?很明显是表达式的操作数和操作符。每一个操作数或操作符都应当作为一个元素储存在ArrayList中。
 =  =  = 元素怎样表示操作符和操作数
需求要求我们精简代码,所以应该将操作符和操作数存储在同一个ArrayList中。一个操作数可以存成一个double型变量,可是加减乘除和括号这样的操作符怎样用double型变量表示,同时不会引起歧义呢?因为字符串表达式中很难出现高精度无理数,所以可以选一个作为表示操作符的常量。笔者选的是:
//用来标识操作符的常数interface OperatorTag{    double ADD =  Math.PI;      //加    double SUB =  Math.PI + 1;  //减    double MUL =  Math.PI + 2;  //乘    double DIV =  Math.PI + 3;  //除    double LBR =  Math.PI + 4;  //左括号    double RBR =  Math.PI + 5;  //右括号}
如果arraylist中的某个元素是上述常数值,则它应当是个操作符。

3.3 按什么顺序存储元素

这个比较明确,按照表达式中出现的顺序存储元素。对于表达式( - 2)*(2 - 2*(3 + 4) + 9*2),它应当存储成:
ArrayList[0] : LBRArrayList[1] : SUBArrayList[2] : 2.0ArrayList[3] : RBRArrayList[4] : MULArrayList[5] : LBRArrayList[6] : 2.0ArrayList[7] : SUBArrayList[8] : 2.0ArrayList[9] : MULArrayList[10]: LBRArrayList[11]: 3.0ArrayList[12]: ADDArrayList[13]: 4.0ArrayList[14]: RBRArrayList[15]: ADDArrayList[16]: 9.0ArrayList[17]: MULArrayList[18]: 2.0ArrayList[19]: RBR
为此我们需要从字符串表达式中提取操作数和操作符,并存到ArrayList中去。

3.4 将字符串表达式转换为ArrayList中的一系列元素

为此我们需要从字符串中提取:
//提取字符串expr中的操作符和操作数并存储到ArrayList<Double> al中public static void parse(String expr){    //去掉所有空格    String str = expr.replace(" ", "");    //标记单目操作符负号位置    int minus_pos = 0;    //将表达式中的所有单目运算符负号转化成双目运算符减号,补充上左操作数0    while( (minus_pos = str.indexOf("(-")) !=  - 1)    {        str = str.substring(0, minus_pos + 1) + "0" + str.substring(minus_pos + 1);    }    //一个操作数、操作符的开始位置    int pos_start = 0;      //一个操作数、操作符的结束位置    int pos_end = 0;        //将字符串转换为字符数组    char[] arr = str.toCharArray();    //逐个提取表达式中的操作数和操作符    while(pos_start != arr.length )    {        pos_end = pos_start;        while( Character.isDigit(arr[pos_start]) == Character.isDigit(arr[pos_end]) || arr[pos_end] == '.')        {            pos_end++;            if(pos_end == arr.length)            {                break;            }        }                //提取到的操作符或操作数字符串        String tmp = new String(arr, pos_start, pos_end - pos_start);                if(Character.isDigit(arr[pos_start]))        {            //将提取出的操作数放入arraylist            al.add(Double.parseDouble(tmp));        }        //将提取出的操作符放入arraylist        else        {            char[] op = tmp.toCharArray();            for(char c : op)            {                if(c == '+')                    al.add(ADD);                else if(c == '-')                    al.add(SUB);                else if(c == '*')                    al.add(MUL);                else if(c == '/')                    al.add(DIV);                else if(c == '(')                    al.add(LBR);                else if(c == ')')                    al.add(RBR);                else                    throw new RuntimeException("Operator not allowd : " + tmp);            }        }        pos_start = pos_end;    }}
这一步中使用了String对象的拼接来补全操作数,包装类Double将字符串转换为它所表示的双精度值,字符数组来定位操作数或操作符的开始结束位置。
我们还要能反向将ArrayList表达式转换回字符串表达式,这样可以方便显示每一步运算结果:
//显示化简到当前一步的, 以arraylist形式存储的表达式alpublic static void display(){    //输出提取到的所有元素‘    for(int i = 0; i < al.size(); i++)    {        if(al.get(i) == ADD)            System.out.print(" + ");        else if(al.get(i) == SUB)            System.out.print(" - ");        else if(al.get(i) == MUL)            System.out.print(" * ");        else if(al.get(i) == DIV)            System.out.print(" / ");        else if(al.get(i) == LBR)            System.out.print("( ");        else if(al.get(i) == RBR)            System.out.print(" )");        else            System.out.print(al.get(i));    }        System.out.println("\n");}

3.5 化简无括号的表达式

我们现在已经将字符串表达式全部转换成名叫al的arraylist中的元素了。下面应当运算al中的元素,将结果写回al,删除al中运算过的元素,这三步就是化简。为了化简有括号的表达式,先要化简无括号的表达式。每化简一步,就是运算一个操作符,将结果写回正确位置并删除用过的多余操作数:
//计算不含括号的表达式的值, 该表达式表示为al中从start到end的一段元素public static void eval(int start, int end){    //元素在当前化简表达式中的位置    int i = start;    //化简的停止位置    int stop = end;    //从左向右计算乘除法,每消去一个操作符则更新al    while(i <= stop)    {        double element = al.get(i);        //临时存放结果        double rslt = 0;                //如果是乘除操作符,则计算该符结果并更新表达式为化简后的        if(element == MUL)            {            rslt = al.get(i - 1) * al.get(i + 1);            //删除操作符和右操作数,左操作数用本步计算结果代替            al.remove(i + 1);            al.remove(i);            al.set(i - 1, rslt);            stop = stop - 2;            display();        }        else if(element == DIV)            {            rslt = al.get(i - 1) / al.get(i + 1);            al.remove(i + 1);            al.remove(i);            al.set(i - 1, rslt);            stop = stop - 2;            display();        }        else        {            i++;        }    }    i = start;    //从左向右计算加减法,每消去一个操作符则更新al    while(i <= stop)    {        double element = al.get(i);        //临时存放结果        double rslt = 0;                //如果是加减操作符,则计算该符结果并更新表达式为化简后的        if(element == ADD)            {   rslt = al.get(i - 1) + al.get(i + 1);            al.remove(i + 1);            al.remove(i);            al.set(i - 1, rslt);            stop = stop - 2;            display();        }        else if(element == SUB)            {            rslt = al.get(i - 1) - al.get(i + 1);            al.remove(i + 1);            al.remove(i);            al.set(i - 1, rslt);            stop = stop - 2;            display();        }        else        {            i++;        }    }}
这个功能用到了arraylist的remove操作。要特别注意删除arraylist元素会改变arraylist长度,而且索引对应的元素也会有变动。删除元素的顺序不同,索引变动也会不同。如果要将索引变动范围限制在remove掉的元素后面而不影响前面,应当从较大的索引开始删除,再删较小的索引。上面的程序在一遍遍历中计算所有乘除法并更新,然后在另一遍遍历中计算所有加减法并更新,到此无括号表达式化简完毕,得到答案。

3.6 化简有括号的表达式

括号是有嵌套关系的,所以确定多组括号的运算顺序就很麻烦。不过我们可以按照无括号表达式化简的思路,化简完一组括号就将其删除,其内部的表达式替换为计算结果。那如何找到最内层的括号呢?按我们的化简替换方法,表达式中最右边的左括号所在的括号组,一定保证其内部的表达式不含括号了。当我们计算替换完了这组括号,在更新的表达式中继续找最右边的左括号,直到所有括号都算完。这时只剩下一个无括号的表达式,用上一步化简无括号表达式的方法就可以了。
括号内部表达式的起止位置作为参数传给无括号表达式的化简方法,就可以实现有括号方法对无括号方法的调用了:
//左右括号位置int lbr_pos = 0;int rbr_pos = 0;//临时存放括号对内表达式计算结果double rslt = 0;//如果表达式中还有括号的话,就从右向左继续计算各个括号对里表达式的值,并替换化简while(al.contains(LBR)){    //最右边的左括号位置    lbr_pos = al.lastIndexOf(LBR);    //其对应的右括号    rbr_pos = lbr_pos + 1;    while(al.get(rbr_pos) != RBR)   rbr_pos++;    //计算该括号对内表达式的值, 并替换化简    eval(lbr_pos + 1, rbr_pos - 1);    //删除括号对    al.remove(lbr_pos + 2);    al.remove(lbr_pos);    display();}//计算所有括号化简掉后的表达式的值eval(0, al.size() - 1);//化简到最后剩下的唯一一个操作数就是答案return  al.get(0);
这段代码不难理解,就是不断寻找下一步要计算的括号,调用无括号方法计算完毕用结果替换后,再寻找更新的表达式中下一步要算的括号。

4. 算术表达式解析器的完整代码和输出

import java.util.*;import java.math.*;//用来标识操作符的常数interface OperatorTag{    double ADD =  Math.PI;      //加    double SUB =  Math.PI + 1;  //减    double MUL =  Math.PI + 2;  //乘    double DIV =  Math.PI + 3;  //除    double LBR =  Math.PI + 4;  //左括号    double RBR =  Math.PI + 5;  //右括号}public class Test implements OperatorTag {       //按表达式中出现的次序存储提取出的操作数和操作符    static ArrayList<Double> al = new ArrayList<Double>();    public static void main(String[] args)     {        //要计算的字符串表达式        String expr = "( - 2.1)*(2 - 2*(3.1 + 4) + 9*2)";        double rslt = evalbr(expr);        System.out.println(rslt);    }    //计算含括号的表达式的值    public static double evalbr(String expr)    {        //解析这个expr表达式,结果存入arraylist        parse(expr);        //显示解析后的arraylist中的表达式        display();                //左右括号位置        int lbr_pos = 0;        int rbr_pos = 0;                //临时存放括号对内表达式计算结果        double rslt = 0;        //如果表达式中还有括号的话,就从右向左继续计算各个括号对里表达式的值,并替换化简        while(al.contains(LBR))        {            //最右边的左括号位置            lbr_pos = al.lastIndexOf(LBR);            //其对应的右括号            rbr_pos = lbr_pos + 1;            while(al.get(rbr_pos) != RBR)   rbr_pos++;            //计算该括号对内表达式的值, 并替换化简            eval(lbr_pos + 1, rbr_pos - 1);            //删除括号对            al.remove(lbr_pos + 2);            al.remove(lbr_pos);            display();        }        //计算所有括号化简掉后的表达式的值        eval(0, al.size() - 1);        //化简到最后剩下的唯一一个操作数就是答案        return  al.get(0);    }    //计算不含括号的表达式的值, 该表达式表示为al中从start到end的一段元素    public static void eval(int start, int end)    {        //元素在当前化简表达式中的位置        int i = start;        //化简的停止位置        int stop = end;        //从左向右计算乘除法,每消去一个操作符则更新al        while(i <= stop)        {            double element = al.get(i);            //临时存放结果            double rslt = 0;                        //如果是乘除操作符,则计算该符结果并更新表达式为化简后的            if(element == MUL)                {                rslt = al.get(i - 1) * al.get(i + 1);                //删除操作符和右操作数,左操作数用本步计算结果代替                al.remove(i + 1);                al.remove(i);                al.set(i - 1, rslt);                stop = stop - 2;                display();            }            else if(element == DIV)                {                rslt = al.get(i - 1) / al.get(i + 1);                al.remove(i + 1);                al.remove(i);                al.set(i - 1, rslt);                stop = stop - 2;                display();            }            else            {                i++;            }        }        i = start;        //从左向右计算加减法,每消去一个操作符则更新al        while(i <= stop)        {            double element = al.get(i);            //临时存放结果            double rslt = 0;                        //如果是乘除操作符,则计算该符结果并更新表达式为化简后的            if(element == ADD)                {   rslt = al.get(i - 1) + al.get(i + 1);                al.remove(i + 1);                al.remove(i);                al.set(i - 1, rslt);                stop = stop - 2;                display();            }            else if(element == SUB)                {                rslt = al.get(i - 1) - al.get(i + 1);                al.remove(i + 1);                al.remove(i);                al.set(i - 1, rslt);                stop = stop - 2;                display();            }            else            {                i++;            }        }    }    //提取字符串中的操作符和操作数并存储到al中    public static void parse(String expr)    {        //去掉所有空格        String str = expr.replace(" ", "");        //标记单目操作符负号位置        int minus_pos = 0;        //将表达式中的所有单目运算符负号转化成双目运算符减号,补充上左操作数0        while( (minus_pos = str.indexOf("(-")) !=  - 1)        {            str = str.substring(0, minus_pos + 1) + "0" + str.substring(minus_pos + 1);        }        //一个操作数、操作符的开始位置        int pos_start = 0;          //一个操作数、操作符的结束位置        int pos_end = 0;            //将字符串转换为字符数组        char[] arr = str.toCharArray();        //逐个提取表达式中的操作数和操作符        while(pos_start != arr.length )        {            pos_end = pos_start;            while( Character.isDigit(arr[pos_start]) == Character.isDigit(arr[pos_end]) || arr[pos_end] == '.')            {                pos_end++;                if(pos_end == arr.length)                {                    break;                }            }                        //提取到的操作符或操作数字符串            String tmp = new String(arr, pos_start, pos_end - pos_start);                        if(Character.isDigit(arr[pos_start]))            {                //将提取出的操作数放入arraylist                al.add(Double.parseDouble(tmp));            }            //将提取出的操作符放入arraylist            else            {                char[] op = tmp.toCharArray();                for(char c : op)                {                    if(c == '+')                        al.add(ADD);                    else if(c == '-')                        al.add(SUB);                    else if(c == '*')                        al.add(MUL);                    else if(c == '/')                        al.add(DIV);                    else if(c == '(')                        al.add(LBR);                    else if(c == ')')                        al.add(RBR);                    else                        throw new RuntimeException("Operator not allowd : " + tmp);                }            }            pos_start = pos_end;        }    }    //显示化简到当前一步的表达式    public static void display()    {        //输出提取到的所有元素‘        for(int i = 0; i < al.size(); i++)        {            if(al.get(i) == ADD)                System.out.print("+");            else if(al.get(i) == SUB)                System.out.print("-");            else if(al.get(i) == MUL)                System.out.print("*");            else if(al.get(i) == DIV)                System.out.print("/");            else if(al.get(i) == LBR)                System.out.print("(");            else if(al.get(i) == RBR)                System.out.print(")");            else                System.out.print(al.get(i));        }            System.out.println("\n");    }}
输出:
(-2.1)*(2-2*(3.1+4)+9*2)(0.0-2.1)*(2.0-2.0*(3.1+4.0)+9.0*2.0)(0.0-2.1)*(2.0-2.0*(7.1)+9.0*2.0)(0.0-2.1)*(2.0-2.0*7.1+9.0*2.0)(0.0-2.1)*(2.0-14.2+9.0*2.0)(0.0-2.1)*(2.0-14.2+18.0)(0.0-2.1)*(-12.2+18.0)(0.0-2.1)*(5.800000000001)(0.0-2.1)*5.800000000001(-2.1)*5.800000000001-2.1*5.800000000001-12.1800000000001

5. 总结

从上面程序的输出明显可以看出double型变量的减法出现了问题,出现了一个很小的余数。这是由double型变量的存储原理导致的。double型变量作为十进制的双精度变量,在计算机中是用一串二进制数字来存储的。这串二进制数字通过双精度公式可以运算得到一个在值上非常接近变量赋值的近似值,并在计算时使用这个近似值。所以double型变量的运算是不精确的。本质的原因是double型变量只是试图用二进制加法得到的近似值去接近用户的赋值,而不是按十进制位逐位保存用户的赋值。为了解决,可以用Math.BigDecimal包装类来代替double型。为了简化代码突出重点,笔者没有这么做,毕竟解析器的算法和容器都证明是无误的。
从这个小项目可见,利用arraylist的可变容量和索引查找这两个优势,用它来表示表达式大大简化了编码和理解的难度。Java API是一本百科全书,限于能力笔者选择arraylist为例来做练习。
0 0