java中的算数运算符号用法与原理分析
一、表达式求值简单说明:
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、浮点数比较是否相等,因为浮点数有精度原因,所以要用精度范围的方式,参考下面代码实现。
三、完整可运行算法实现如下:
package com.begoina.zero;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;/*** 加减乘除表达式求值算法* @date 2018年7月7日* @version 1.0*/
public class Calculator {// 表达式字符合法性校验正则模式,静态常量化可以降低每次使用都要编译地消耗private static final Pattern EXPRESSION_PATTERN = Pattern.compile("[0-9\\.+-/*()= ]+");// 运算符优先级mapprivate 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(); // 当前参与计算数值2BigDecimal num1 = numStack.pop(); // 当前参与计算数值1BigDecimal 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(); // 当前参与计算数值2BigDecimal num1 = numStack.pop(); // 当前参与计算数值1BigDecimal 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 num1* @param num2* @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 = ")));}private Calculator(){};}
————————————————
原文链接:https://blog.csdn.net/moon_1991/article/details/80947858
java中的算数运算符号用法与原理分析相关推荐
- Java中的~运算符号
最近做的笔试题中出现这样一道题: 题目:What results from the following code fragment?int i = 5;int j = 10;System.out.pr ...
- 【Java学习笔记之二十九】Java中的equals和==的用法及区别
Java中的"equals"和"=="的用法及区别 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String(&quo ...
- Linux有关Shell算数运算的用法补充笔记
1.自增自减 Shell的自增自减和其他编程语言的语法基本上是一样的.主要包括四种:前置自增.前置自减.后置自增.后置自减.前置的原理是先修改变量的值,然后将变量的值传递出去.后置的原理是先将变量的值 ...
- Java中BigDecimal类介绍及用法
Java中BigDecimal类介绍及用法 Java中提供了大数字(超过16位有效位)的操作类,即 java.math.BinInteger 类和 java.math.BigDecimal 类,用于高 ...
- Java中「与运算,或运算,异或运算,取反运算。」
Java中「与运算,或运算,异或运算,取反运算.」 文章目录 Java中「与运算,或运算,异或运算,取反运算.」 Java中的「与运算(AND) & 」 规则 :都为1时才为1,否则为0 Ja ...
- Java中的 移位 运算
Java中的 移位 运算 正数 左移 右移 无符号右移 负数 右移 无符号右移 左移 下面通过代码来演示: (在注释中 会标明 移位运算的 一些理论 ) public class Move {publ ...
- Java 位运算理解 Java中的位移运算整理 Java右移n位 Java左移n位
Java 位运算理解 Java中的位移运算整理 Java右移n位 Java左移n位 一.概述 1.在浏览一篇文章时,看到一个介绍 ,使用位移操作替代乘除法 ,若位移多位该怎么计算呢? 二.代码理解 ...
- Java中PreparedStatement和Statement的用法区别
Java中PreparedStatement和Statement的用法区别 (2012-08-01 11:06:44) 转载▼ 标签: 杂谈 1. PreparedStatement接口继承Sta ...
- shell中的算数运算
shell中的算数运算 首先这里所说的算术运算是指加法(+), 减法(-),乘法(*),整除(/), 求余(%)四种运算,可以通过添加括号改变优先级,并且只能对整数进行运算(Linux Bash Sh ...
最新文章
- API读取写入 ini文件内容的方法函数详解
- python 二维矩阵翻转
- iOS开发之功能模块--推送之坑问题解决
- php在线客服系统源码_在线客服系统物流行业解决方案
- MTK平台APP层 通过INvram获取SN号
- gridView删除提示框
- 迷你linux操作系统,自己动手做一个迷你型Linux操作系统
- rundll32的使用和使用c#调用dll
- 史上最全MySQL锁机制
- linux系统vnc无法远程桌面,linux下vnc远程桌面连接方法分享
- Qt表格中以旋转框的形式数据交互
- SpringMVC jsp界面值渲染不出来
- 深度学习自学(八):人脸识别主要场景与算法实现
- SQL SERVER 查找某个字符在字符串中出现的次数
- nGrinder Loadrunner vs nGrinder
- Android编程 移动应用开发 经典习题案例 (附案例 注意点)
- G950U破解电信4G正确姿势
- “2021云管和云网大会”在京召开
- 极海单片机串口调试记录
- 利用IPv6的地址特性写一个攻击甩锅程序
热门文章
- SQL Server中的文件流
- 了解SQL Server中的GUID数据类型
- sql join 示例_SQL CROSS JOIN与示例
- Spring注解@ConfigurationPropertie
- mysql 约束条件 外键 forigen key 介绍
- [靠谱原创!] SSH免密登录设置----原理详解+具体操作(全国人民看完都懂了!)
- ContentType明细对照表(文件类型相关的设置)
- spring加载jar包中多个配置文件(转)
- NHibernate Linq中Null值排序的解决方法
- 英1数1专业课408计算机考研312分,408计算机考研复习经验:各个突破全面掌握