首页 > 编程语言 >Java实现的表达式求值算法(包括加减乘除以及括号运算)

Java实现的表达式求值算法(包括加减乘除以及括号运算)

时间:2024-03-29 10:37:13浏览次数:30  
标签:numStack Java 运算 optStack 数值 运算符 求值 表达式 加减乘除

package com.example.demo.java;
 
 
import com.greenpineyu.fel.FelEngine;
import com.greenpineyu.fel.FelEngineImpl;
 
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * @author Anzepeng
 * @title: Calculator
 * @projectName demo
 * @description: TODO
 * @date 2020/6/10 0010下午 15:02
 */
public class Calculator {
 
    // 表达式字符合法性校验正则模式,静态常量化可以降低每次使用都要编译地消耗
    private static final Pattern EXPRESSION_PATTERN = Pattern.compile("[0-9\\.+-/*()= ]+");
 
    // 运算符优先级map
    private static final Map<String, Integer> OPT_PRIORITY_MAP = new HashMap<String, Integer>() {
        private static final long serialVersionUID = 6968472606692771458L;
 
        {
            put("(", 0);
            put("+", 2);
            put("-", 2);
            put("*", 3);
            put("/", 3);
            put(")", 7);
            put("=", 20);
        }
    };
 
 
    /**
     ** 输入加减乘除表达式字符串,返回计算结果
     ** @param expression 表达式字符串
     ** @return 返回计算结果
     *      
     */
    public static double executeExpression(String expression) {
        // 非空校验
        if (null == expression || "".equals(expression.trim())) {
            throw new IllegalArgumentException("表达式不能为空!");
        }
 
        // 表达式字符合法性校验
        Matcher matcher = EXPRESSION_PATTERN.matcher(expression);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("表达式含有非法字符!");
        }
 
        Stack<String> optStack = new Stack<>(); // 运算符栈
        Stack<BigDecimal> numStack = new Stack<>(); // 数值栈,数值以BigDecimal存储计算,避免精度计算问题
        StringBuilder curNumBuilder = new StringBuilder(16); // 当前正在读取中的数值字符追加器
 
        // 逐个读取字符,并根据运算符判断参与何种计算
        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            if (c != ' ') { // 空白字符直接丢弃掉
                if ((c >= '0' && c <= '9') || c == '.') {
                    curNumBuilder.append(c); // 持续读取一个数值的各个字符
                } else {
                    if (curNumBuilder.length() > 0) {
                        // 如果追加器有值,说明之前读取的字符是数值,而且此时已经完整读取完一个数值
                        numStack.push(new BigDecimal(curNumBuilder.toString()));
                        curNumBuilder.delete(0, curNumBuilder.length());
                    }
 
                    String curOpt = String.valueOf(c);
                    if (optStack.empty()) {
                        // 运算符栈栈顶为空则直接入栈
                        optStack.push(curOpt);
                    } else {
                        if (curOpt.equals("(")) {
                            // 当前运算符为左括号,直接入运算符栈
                            optStack.push(curOpt);
                        } else if (curOpt.equals(")")) {
                            // 当前运算符为右括号,触发括号内的字表达式进行计算
                            directCalc(optStack, numStack, true);
                        } else if (curOpt.equals("=")) {
                            // 当前运算符为等号,触发整个表达式剩余计算,并返回总的计算结果
                            directCalc(optStack, numStack, false);
                            return numStack.pop().doubleValue();
                        } else {
                            // 当前运算符为加减乘除之一,要与栈顶运算符比较,判断是否要进行一次二元计算
                            compareAndCalc(optStack, numStack, curOpt);
                        }
                    }
                }
            }
        }
 
        // 表达式不是以等号结尾的场景
        if (curNumBuilder.length() > 0) {
            // 如果追加器有值,说明之前读取的字符是数值,而且此时已经完整读取完一个数值
            numStack.push(new BigDecimal(curNumBuilder.toString()));
        }
        directCalc(optStack, numStack, false);
        return numStack.pop().doubleValue();
    }
 
    /**
     ** 拿当前运算符和栈顶运算符对比,如果栈顶运算符优先级高于或同级于当前运算符,
     ** 则执行一次二元运算(递归比较并计算),否则当前运算符入栈
     ** @param optStack 运算符栈
     ** @param numStack 数值栈
     ** @param curOpt 当前运算符
     *      
     */
    public static void compareAndCalc(Stack<String> optStack, Stack<BigDecimal> numStack,
                                      String curOpt) {
        // 比较当前运算符和栈顶运算符的优先级
        String peekOpt = optStack.peek();
        int priority = getPriority(peekOpt, curOpt);
        if (priority == -1 || priority == 0) {
            // 栈顶运算符优先级大或同级,触发一次二元运算
            String opt = optStack.pop(); // 当前参与计算运算符
            BigDecimal num2 = numStack.pop(); // 当前参与计算数值2
            BigDecimal num1 = numStack.pop(); // 当前参与计算数值1
            BigDecimal bigDecimal = floatingPointCalc(opt, num1, num2);
 
            // 计算结果当做操作数入栈
            numStack.push(bigDecimal);
 
            // 运算完栈顶还有运算符,则还需要再次触发一次比较判断是否需要再次二元计算
            if (optStack.empty()) {
                optStack.push(curOpt);
            } else {
                compareAndCalc(optStack, numStack, curOpt);
            }
        } else {
            // 当前运算符优先级高,则直接入栈
            optStack.push(curOpt);
        }
    }
 
    /**
     ** 遇到右括号和等号执行的连续计算操作(递归计算)
     ** @param optStack 运算符栈
     ** @param numStack 数值栈
     ** @param isBracket true表示为括号类型计算
     *      
     */
    public static void directCalc(Stack<String> optStack, Stack<BigDecimal> numStack,
                                  boolean isBracket) {
        String opt = optStack.pop(); // 当前参与计算运算符
        BigDecimal num2 = numStack.pop(); // 当前参与计算数值2
        BigDecimal num1 = numStack.pop(); // 当前参与计算数值1
        BigDecimal bigDecimal = floatingPointCalc(opt, num1, num2);
 
        // 计算结果当做操作数入栈
        numStack.push(bigDecimal);
 
        if (isBracket) {
            if ("(".equals(optStack.peek())) {
                // 括号类型则遇左括号停止计算,同时将左括号从栈中移除
                optStack.pop();
            } else {
                directCalc(optStack, numStack, isBracket);
            }
        } else {
            if (!optStack.empty()) {
                // 等号类型只要栈中还有运算符就继续计算
                directCalc(optStack, numStack, isBracket);
            }
        }
    }
 
    /**
     ** 不丢失精度的二元运算,支持高精度计算
     ** @param opt
     ** @param bigDecimal1
     ** @param bigDecimal2
     ** @return
     *      
     */
    public static BigDecimal floatingPointCalc(String opt, BigDecimal bigDecimal1,
                                               BigDecimal bigDecimal2) {
        BigDecimal resultBigDecimal = new BigDecimal(0);
        switch (opt) {
            case "+":
                resultBigDecimal = bigDecimal1.add(bigDecimal2);
                break;
            case "-":
                resultBigDecimal = bigDecimal1.subtract(bigDecimal2);
                break;
            case "*":
                resultBigDecimal = bigDecimal1.multiply(bigDecimal2);
                break;
            case "/":
                resultBigDecimal = bigDecimal1.divide(bigDecimal2, 10, BigDecimal.ROUND_HALF_DOWN); // 注意此处用法
                break;
            default:
                break;
        }
        return resultBigDecimal;
    }
 
    /**
     ** priority = 0  表示两个运算符同级别
     ** priority = 1  第二个运算符级别高,负数则相反
     ** @param opt1
     ** @param opt2
     ** @return
     *      
     */
    public static int getPriority(String opt1, String opt2) {
        int priority = OPT_PRIORITY_MAP.get(opt2) - OPT_PRIORITY_MAP.get(opt1);
        return priority;
    }
 
    /**
     ** 浮点数相等比较函数
     ** @param value1
     ** @param value2
     ** @return
     *      
     */
    private static boolean isDoubleEquals(double value1, double value2) {
        System.out.println("正确结果=" + value1 + ", 实际计算结果=" + value2);
        return Math.abs(value1 - value2) <= 0.0001;
    }
 
    public static void main(String[] args) {
        // 几个测试数据
        System.out.println(isDoubleEquals(-39.5, executeExpression(" 2 + 3/2 * 3 - 4 *(2 + 5 -2*4/2+9) + 3 + (2*1)-3= ")));
        System.out.println(isDoubleEquals(60.3666, executeExpression("9*2+1/3*2-4+(3*2/5+3+3*6)+3*5-1+3+(4+5*1/2)=")));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199 = ")));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199" )));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199")));
        System.out.println(isDoubleEquals(-29, executeExpression(" 9 *(20-1)-(1+199) ")));
        System.out.println(isDoubleEquals(1.0E24, executeExpression("1000000000000*1000000000000 = ")));
    }
}

 

一、表达式求值简单说明:
1、求值表达式主要包括加减乘除四种基本运算,其实表达式可以看做由一个个二元运算构成,前一个二元运算的结果作为后一个二元运算的输入。

        举个例子: “1+2-4=”,“1+2”就是一个二元运算,1和2是操作数,+是运算符,它们的运算结果3作为下一个二元运算的输入,所以下一个二元运算是“3-4”,这样经过两次二元运算后得出结果-1,此时碰到表达式结束符号“=”,那么表达式运算结束,最终值为-1。当然有时候表达式不是以等号作为结束符号的,这种场景要特别注意下。
 

2、加减乘除四种运算符是有优先级的,乘法和除法同级,且比加法和减法优先级高,也就是说同级的运算顺序是从左到右,高优先级的二元运算优先执行。

        举个例子:“1+2*3=”,“2*3”这个二元运算因为乘法的优先级高,所以优先执行,执行结果作为+二元运算的第二个操作数。
 

3、括号运算其实相当于嵌套一个子表达式,而子表达式的优先级比括号外的加减乘数二元运算高,子表达式的计算结果作为主表达式的一个操作数。
 

4、由上面的说明可知,表达式有两个基本元素,那就是操作数和运算符,运算符一般占用一个字符,而操作数可能是1, 2, 3这样的个位整数,也可能是1.0, 1.89这样的浮点数,也可能是100, 189这样的多位整数。所以我们在从表达式解析出操作数时要完整的解析出整个操作数。
 

5、表达式一般以等号作为表达式结束标记,当然有不以等号作为结束标记的场景,详情看下面代码处理逻辑。

 


 

二、算法思想简单说明:
1、表达式由正则模式 "[0-9\\.+-/*()= ]+" 来校验合法性。
 

2、表达式的操作数将push到一个数值栈中,而运算符将push到运算符栈中,解析表达式时,采用逐个读取字符的形式,特别注意操作数是多位字符的场景,可以采用一个追加器将字符先缓存起来,当完整读取一个数值时再讲数值push到数值栈中。
 

3、当准备push到运算符栈的当前运算符,优先级同级于或低于栈顶运算符时,将触发一次二元运算,参与二元运算的为运算符栈的栈顶元素以及数值栈栈顶的两个操作数。二元运算得支持高精度运算,同时避免精度丢失问题。

        举个例子: “2 * 3 - 1=”,在读取到运算符 “-” 前,运算符栈中已有元素[“*”],数值栈中有元素[“3”、“2”],因为运算符“-”的优先级比栈顶运算符“*”的优先级低,所以触发二元运算 “2 * 3”,相关操作数和运算符出栈,运算结果“6”作为新的操作数push到数值栈中。特别注意,此时如果运算符栈中栈顶还有元素,那么优先级比对还得继续,这是一个递归操作,直到运算符栈没有元素或者当前运算符“-”的优先级高于栈顶元素,那么当前运算符push到运算符栈,继续读取表达式的下一个元素。
 

4、括号运算,相等于子表达式运算,当表达式解析到左括号时,将左括号push到运算符栈,当解析到右括号时,将递归运算整个子表达式的所有二元运算操作,直到碰到左括号才停止,此时子表达式的计算结果作为新的操作数push到数值栈中。

      举个例子: “2 * (3 - 1*2)=”,在解析到右括号“)”前,运算符栈中已有元素[“*”,“-”,“(”,“*”],数值栈已有元素[“2”、“1”、“3”、“2”]。 【温馨提示,这里元素从左到右依次表示从栈顶到栈底,没有用更直观的图示,请见谅。】

        当碰到右括号时,触发一次二元运算,即“1*2”,运算符栈的栈顶元素“*”出栈,数值栈的栈顶元素“2”和“1”分别出栈,二元运算得出结果“2”,结果值push到数值栈。此时运算符栈中已有元素[“-”,“(”,“*”],数值栈已有元素[“2”、“3”、“2”]。

        子表达式还没运算结束,继续递归触发二元运算,即“3-2”,运算符栈的栈顶元素“-”出栈,数值栈的栈顶元素“2”和“3”分别出栈,二元运算得出结果“1”,结果值push到数值栈。此时运算符栈中已有元素[“(”,“*”],数值栈已有元素[“1”、“2”]。

        继续读取运算符栈顶元素,发现是左括号“(”,此时栈顶元素出栈,而无需触发二元计算,此时运算符栈中已有元素[“*”],数值栈已有元素[“1”、“2”]。

        表达式继续解析,此时发现读取到等号“=”,递归触发表达式的所有二元运算,即“2*1”,得出最终结果2,计算结束。
 

5、浮点数比较是否相等,因为浮点数有精度原因,所以要用精度范围的方式,参考下面代码实现。

 

标签:numStack,Java,运算,optStack,数值,运算符,求值,表达式,加减乘除
From: https://www.cnblogs.com/ynhk/p/18103232

相关文章

  • 中文地址智能分词算法-Java版
    addresstool用于处理中文地址的分词和匹配,采用NLP分词算法进行中文地址分词,在中文地址分词基础上对垃圾地址进行识别,过滤,标记等。根据杂乱的业务地址自动化生成标准中文地址库,并支持随机业务地址关联到标准地址操作,实测速度达到25000条/每秒今天我们测试一下addresstool的地址分......
  • 【华为OD机试真题】C卷-虚拟理财游戏(JAVA)
    一、题目描述【华为OD机试真题】C卷-虚拟理财游戏(JAVA)题目描述:在一款虚拟游戏中生活,你必须进行投资以增强在虚拟游戏中的资产以免被淘汰出局。现有一家Bank,它提供有若干理财产品m个,风险及投资回报不同,你有N(元)进行投资,能接收的总风险值为X。你要在可接受范围内选择最优......
  • 2. Java基本语法
    文章目录2.Java基本语法2.1关键字保留字2.1.1关键字2.1.2保留字2.1.3标识符2.1.4Java中的名称命名规范2.2变量2.2.1分类2.2.2整型变量2.2.3浮点型2.2.4字符型char2.2.5Unicode编码2.2.6UTF-82.2.7boolean类型2.3基本数据类型转换2.3.1自动类型转换2.2......
  • 业务地址关联匹配标准地址库-Java
    地名作为最常用的社会公共信息,不仅与人们的日常生活息息相关,而且是政府行政行为、经济建设不可缺少的基础信息资源。在政务系统中有许多需要将业务地址关联到标准地址的场景,addresstool致力于解决地址关联匹配算法中的速度和准确性问题。经实测,addresstool的地址关联速度在5000/秒......
  • 解决import javax.swing.JTable;偶发性复制不了的问题
    解决方法:重写JTable类的键盘监听事件。 /** *20240313addhzh */ table.addKeyListener(newKeyListener(){ @Override publicvoidkeyPressed(KeyEvente){ //System.out.println("22222"); System.out.println("keycode"+e.getKeyCode())......
  • 聊聊 Java GC
    [1]序GC是每一个Java程序员不可绕过的话题。GC是在某些时候对内存的垃圾对象数据进行搜寻定位,然后进行内存空间回收。根据这个定义,则学习GC相关知识,需要关注:对JVM整个内存结构中哪些区域进行垃圾回收;在这些内存区域中的类数据或者实例数据等数据结构是什么样子的;然后想想如......
  • java 16进制转换为ASCII
    /***16进制转换为ASCII**@parambytes源数据*@paramoffset偏移量*@paramlength数据长度*@return{@linkString}*/publicStringconvertHexToAscii(byte[]bytes,intoffset,intlength){StringBuilders......
  • 《手把手教你》系列技巧篇(五十九)-java+ selenium自动化测试 - 截图三剑客 -上篇(详细教
    1.简介今天本来是要介绍远程测试的相关内容的,但是宏哥在操作服务器的时候干了件糊涂的事,事情经过是这样的:本来申请好的Windows服务器用来做演示的,可是服务器可能是局域网的,连百度都不能访问,宏哥想调试一下网络,禁用网卡,然后重启网卡,结果禁用后就连不上了。。。。就只能等服......
  • 《手把手教你》系列技巧篇(六十)-java+ selenium自动化测试 - 截图三剑客 -中篇(详细教程
    1.简介前面我们介绍了Selenium中TakeScreenshot类来截图,得到的图片是浏览器窗口内的截图。有时候,只截浏览器窗口内的图是不够的,而且TakeScreenshot截图只针对浏览器的web事件,假如你在运行脚本过程,windows上有一个其他软件弹出了一个购物的弹窗页面,这个时候就会干扰你......
  • java实现字节数组转int(采用IEEE 754标准)
    /***字节数组转int*采用IEEE754标准**@parambytes*@returnfloat*/publicintbytesToInt(byte[]bytes){//获取字节数组转化成的2进制字符串StringbinaryStr=bytesToBinaryStr(bytes);//符号位......