本文已在本人微信公众号“码农小阿飞”上发布,打开微信搜索“码农小阿飞”,或者扫描文章结尾的二维码,进入公众号并关注,就能够在第一时间收到个人推文!
前段时间,在逛论坛的时候,看到一个比较有趣的提问:怎么用代码实现一个程序,能够根据用户动态输入的传统算术表达式,去解析并计算这个表达式,最后,给用户返回一个计算结果?固然了,这个算术表达式比较简单,运算操做符只有+-*/()
。java
例如:用户输入的字符串表达式为5+2*(1+3*(5-1*2))
,程序运行结束给用户返回一个计算结果25
。那是怎么计算得出这么一个结果的呢?正则表达式
“先乘除后加减,有小括号要先计算小括号里的”,幼儿园小朋友可能都知道的逻辑。但是,对计算机来讲,只能经过循环和判断等方式来解决问题,怎么用代码来让计算机来根据这个规则去运算呢?express
由于算术表达式的计算是有前后顺序关系,必须先找到表达式中优先级高的运算操做,先计算得出结果,再考虑优先级较低的运算操做,这就涉及到一个寻找匹配的过程,所以天然而然可以想到的就是用正则表达式。编程
在正则表达式中,用\\([^\\(\\)]*\\)
匹配一对没有嵌套的单层括号对,这应该好理解,一对小括号,中间有一串表达式,可是再也不嵌套其余小括号。用\\-?\\d+(\\*|\\/)\\-?\\d+
匹配乘除运算,用\\-?\\d+(\\+|\\-)\\-?\\d+
匹配加减运算。这也好理解,经过+-*/
链接的两个数值串,固然,数值多是负数因此数值的模式串为\\-?\\d+
。数组
最后,只要依次判断这三个正则表达式在当前表达式中是否存在,若是存在,则把匹配的内容取出来作相应的计算操做,并将结果替换原来匹配出来的子串。微信
举个简单的例子,但凡能被正则表达式\\-?\\d+\\*\\-?\\d+
匹配的字符串必定知足格式a*b
(a和b都是数值,固然多是负的),咱们只须要以*
为分隔符,将子串a和b分隔提取出来,再将子串a和b都转换成int整形进行相应的计算。数据结构
if (expression.matches("\\-?\\d+\\*\\-?\\d+")) { String[] split = expression.split("\\*"); int value1 = Integer.parseInt(split[0]); int value2 = Integer.parseInt(split[1]); return value1 * value2; }
最后再用这个计算结果替换原表达式中的a*b
,除法、加法、减法也是这么处理,一步一步以此类推,先匹配再运算,直到表达式再也匹配不到乘除运算和加减运算,就能够输出结果了。工具
这是我用正则表达式实现的程序,根据输入打印的结果:
能够看出来,和咱们常规计算算术表达式时的思路同样。ui
固然,实际代码操做中还有很多细节须要处理,由于篇幅有限,也不是本篇推崇的实现方案,这里提出一种思路,就不在此处作代码展现,对正则表达式实现感兴趣或者想要去了解的朋友能够私信我,一块儿讨论。spa
虽然,看起来轻松写意,寥寥几语就把实现方案描绘出来,但用正则表达式匹配字符串整体上仍是比较繁琐,效率也比较低,很容易在写匹配模式串的时候,由于考虑不到的状况,致使整个表达式没法解开,并且在拓展更多操做符的时候,也不是太方便。那到底会不会存在更简洁优雅的方式去实现呢?答案是确定的,那就是今天我要推荐给大伙儿的后缀表达式,一个为计算机执行算术运算而生的表达式。
后缀大伙儿应该都知道,在英语中能够根据单词的后缀区分词性,在计算机中能够根据文件名的后缀区分文件类型,但是,后缀表达式又是什么呢?它们之间又有着什么样的联系呢?
先来看一个表达式:1 2 3 + 4 * + 5 -
。你没看错,我也没有写错,这的的确确是一个表达式,这是就是算术表达式1+((2+3)*4)-5
对应的后缀表达式,由于表达式中操做运算符都位于须要进行运算操做的数值的后边(右侧),于是得名后缀表达式。
后缀表达式有一个运算规则:从左往右依次遍历后缀表达式,若是遍历到的元素是数值的话,将数值入栈到一个数值栈中。若是遍历到的元素是运算符的话,取出数值栈栈顶的前两个数值,以"次顶元素 运算符 栈顶元素"的位置关系,作相应的算术运算,并把运算的结果入栈到数值栈中,直到遍历到后缀表达式的末端,再将栈顶的元素取出,即是运算的结果。咱们先根据规则,运行上述后缀表达式,运行过程步骤以下图所示:
/*** * 解析计算给定的后缀表达式 * * @param rpnExpression 后缀表达式 * */ public int compute(String[] rpnExpression) { Objects.requireNonNull(rpnExpression, "后缀表达式为空"); Stack<Integer> stack = new Stack<>(); int value1, value2; for (String string : rpnExpression) { switch (string) { case "+": { if (2 > stack.size()) { throw new RuntimeException("解析异常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 + value2); break; } case "-": { if (2 > stack.size()) { throw new RuntimeException("解析异常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 - value2); break; } case "*": { if (2 > stack.size()) { throw new RuntimeException("解析异常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 * value2); break; } case "/": { if (2 > stack.size()) { throw new RuntimeException("解析异常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 / value2); break; } default: { stack.push(Integer.parseInt(string)); break; } } } if (1 != stack.size()) { throw new RuntimeException("解析异常"); } return stack.pop(); }
能够发现,运算的结果是和表达式1+((2+3)*4)-5
的结果同样。
对比传统的算术表达式运算,后缀表达式确实知足运算符的前后顺序,而且计算机执行起来更加简洁方便,只要简单的从左往右遍历表达式,就能计算出结果,避免了使用正则表达式去处理时由于匹配优先级各类字符串匹配的过程。那究竟是怎么从一个算术表达式推导出一个后缀表达式的呢?
一样,后缀表达式的推导也一样有一个规则:这里须要初始化两个辅助工具一个队列和一个堆栈,分别是后缀表达式输出队列(先进先出)和操做符暂存栈(先进后出)。
从左往右依次遍历算术表达式,若是遍历到的元素是数值的话,直接入队到输出队列中,若是遍历到的元素是操做符的话,状况比较复杂一些,须要考虑这些操做运算符的优先级:
若是当前遍历到的操做符是+-
的话,由于优先级相对较低,只有在操做符堆栈为空或者栈顶操做符为(
的时候才能入栈。若是栈顶操做符的优先级大于或等于当前操做符的话,则将栈顶操做符出栈,并入队到后缀表达式输出队列。在栈顶操做符出栈后,当前操做符继续和新的栈顶操做符比较,以此类推,直到达到入栈标准。
若是当前遍历到的操做符是*/
的话,思想和上述+-
同样,可是由于*/
的优先级相对较高,因此入栈的条件相对较低,只要堆栈为空,或者只要栈顶元素不是*/
都能入栈。不然,将栈顶操做符出栈,并入队到后缀表达式输出队列。在栈顶操做符出栈后,当前操做符继续和新的栈顶操做符比较,以此类推,直到达到入栈标准。
这里最特殊的当属操做符()
,若是当前遍历到的操做符是(
的话,不论什么状况,直接入栈。若是当前遍历到的操做符为)
的话,操做符堆栈内距离栈顶最近的那个操做符(
和栈顶组成的一个区间内全部的操做符,依次出栈,并入队到后缀表达式输出队列。出栈完毕后,将栈顶的操做符(
出栈,并舍去。这也就是后缀表达式明明没有小括号,却一样能实现本来算术表达式中小括号内的运算优先的保证。
/*** * 将中缀表达式转换为后缀表达式 * */ public String[] parse2rpn(String[] expressionArray) { Objects.requireNonNull(expressionArray, "算术表达式数组为空"); Queue<String> queue = new LinkedList<>(); Stack<String> stack = new Stack<>(); for (String string : expressionArray) { if (!"+".equals(string) && !"-".equals(string) && !"*".equals(string) && !"/".equals(string) && !"(".equals(string) && !")".equals(string)) { // 非操做符直接输出到队列 queue.offer(string); continue; } if ("+".equals(string) || "-".equals(string)) { while (true) { // 加减符号只有在空栈,或者栈顶操做符为'('的状况下可以入栈 if ((0 >= stack.size() || "(".equals(stack.peek()))) { stack.push(string); break; } else { queue.offer(stack.pop()); } } continue; } if ("*".equals(string) || "/".equals(string)) { while (true) { // 乘除符号只要栈顶符号不是乘除符号,都能入栈 if ((0 >= stack.size() || (!"*".equals(stack.peek()) && !"/".equals(stack.peek())))) { stack.push(string); break; } else { queue.offer(stack.pop()); } } continue; } // 左括号任何状况直接入栈 if ("(".equals(string)) { stack.push(string); continue; } while (true) { if (0 >= stack.size()) { throw new RuntimeException("表达式异常"); } if (!"(".equals(stack.peek())) { queue.offer(stack.pop()); } else { stack.pop(); break; } } } while (0 < stack.size()) { queue.offer(stack.pop()); } return queue.toArray(new String[0]); }
后缀表达式在推导过程当中,巧妙的运用了数据结构中栈先进后出的特性,将优先级较高的操做符放置在栈顶,在出栈的时候,栈顶的操做符优先入队到输出队列中,这也就知足了从左往右遍历后缀表达式的时候优先级较高的操做符在左侧优先计算。所以,在后续运行的时候就不须要再去考虑优先级问题,从左往右执行运算操做就行。
能够发现,实现从传统算术表达式也到后缀表达式的转换并不难,虽然可读性降低了,可是却能使计算机理解起来简单,下降编写程序的复杂性,提升运行的效率。
文章到这里也该结束了,若是以为这篇文章对你了解后缀表达式或者在平常解决问题有帮助的话,不胜荣幸。固然,若是对文章有什么疑问,又或者是文章中有什么地方有误或者解释不当,请在评论区留言,一块儿探讨。
关注我,带你去看编程世界中那些有趣的发现。