最近一次在重构过程中,遇到一个功能,它实现对字符串表达式的计算,对类似 (a+b)*c 这种表达式进行实时计算,老的方式采用分割字符串的方式来实现,经常出错,我改写了一下。采用了几种方式。在此记录下来。
1.正则分组
使用正则的组匹配类似于 ((.+))(.+) 匹配之后结果为
根据正则组的顺序来计算。
但是对于这种也是有很多缺点的,一旦表达式复杂起来,正则将变得异常复杂和难以调试。而且,这种方式也必须事先规定字符串的格式。
2.后缀表达式
我们平时写的这种 (a+b)*c 表达式叫做中缀表达式,与之相对的有前缀表达式和后缀表达式,后缀表达式也叫逆波兰式。
它的原理是将数字计算符后移,并将括号中的运算符至于前面,即不使用括号来保证运算顺序的一种方式,这种方式很适合计算机运算。
例如,将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下:
扫描到的元素 | S2(栈底->栈顶) | S1 (栈底->栈顶) | 说明 |
1 | 1 | 空 | 数字,直接入栈 |
+ | 1 | + | S1为空,运算符直接入栈 |
( | 1 | + ( | 左括号,直接入栈 |
( | 1 | + ( ( | 同上 |
2 | 1 2 | + ( ( | 数字 |
+ | 1 2 | + ( ( + | S1栈顶为左括号,运算符直接入栈 |
3 | 1 2 3 | + ( ( + | 数字 |
) | 1 2 3 + | + ( | 右括号,弹出运算符直至遇到左括号 |
× | 1 2 3 + | + ( × | S1栈顶为左括号,运算符直接入栈 |
4 | 1 2 3 + 4 | + ( × | 数字 |
) | 1 2 3 + 4 × | + | 右括号,弹出运算符直至遇到左括号 |
- | 1 2 3 + 4 × + | - | -与+优先级相同,因此弹出+,再压入- |
5 | 1 2 3 + 4 × + 5 | - | 数字 |
到达最右端 | 1 2 3 + 4 × + 5 - | 空 | S1中剩余的运算符 |
因此结果为“1 2 3 + 4 × + 5 -”(注意需要逆序输出)。
详细原理参考:http://blog.csdn.net/antineutrino/article/details/6763722/
在对比一系列实现之后,这里我使用栈来实现这种方式。
package suffixformular; import java.util.Collections; import java.util.Stack; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import javax.script.ScriptException; /** * 后缀表达式原理 */ public class SuffixFormula { private static SuffixFormula instance = new SuffixFormula(); private SuffixFormula() { } public static SuffixFormula getInstnace() { return instance; } private Stack transformToSuffix(String infix) { InToPost inToPost = new InToPost(infix); return inToPost.doTrans(); } public void c(Stack<Double> sk, String ch) { switch (ch) { case "+": try { sk.push(sk.pop() + sk.pop()); } catch (Exception e) { e.printStackTrace(); } break; case "-": double tmp = sk.pop(); sk.push(sk.pop() - tmp); break; case "*": sk.push(sk.pop() * sk.pop()); break; case "/": double temp = sk.pop(); sk.push(sk.pop() / temp); break; case "^": double tp = sk.pop(); sk.push(Math.pow(sk.pop(), tp)); break; case "%": double mp = sk.pop(); sk.push(sk.pop() % mp); break; default: throw new RuntimeException(); } } // 完整版 public double formula(String infix) { Stack result = new Stack<Double>(); Stack suffix = transformToSuffix(infix); Collections.reverse(suffix); Pattern pattern = Pattern.compile("[0-9]*"); // ① while (!suffix.isEmpty()) { String str = String.valueOf(suffix.pop()); if (pattern.matcher(str).matches()) { double dou = Double.parseDouble(str); result.push(dou); } else { c(result, str); } } return (double) result.pop(); } public static void main(String[] args) { String abc = "2^((4+2)/3)"; //"1+((2+3)*4)-5";// double value = // 0; SuffixFormula.getInstnace().formula(abc); System.err.println(value); } } class InToPost { private Stack stack; private String input;// 输入中缀表达式 private Stack output;// 输出的后缀表达式 public InToPost(String input) { this.input = input; int size = input.length(); stack = new Stack<Object>(); output = new Stack<Object>(); } public Stack doTrans() {// 转换为后缀表达式方法 boolean ischar = false; for (int i = 0; i < input.length(); i++) { char ch = input.charAt(i); if (ch == ' ') { continue; } //负号部分,未测试 // else if (ch == '-' && (output.size()<=0 || ischar)) { // output.push(ch); // // ischar = false; // continue; // } switch (ch) { case '+': case '-': getOper(ch, 1); ischar = true; break; case '*': case '/': case '%': getOper(ch, 2); ischar = true; break; case '^': getOper(ch, 3); ischar = true; break; case '(': stack.push(ch); ischar = true; break; case ')': getParent(ch); ischar = true; break; default: output.push(ch); ischar = false; break; } } while (!stack.isEmpty()) { output.push(stack.pop()); } return output; } public void getParent(char ch) { while (!stack.isEmpty()) { char chx = (char) stack.pop(); if (chx == '(') { break; } else { output.push(chx); } } } public void getOper(char ch, int prec1) { while (!stack.isEmpty()) {// 判断栈是否为空 char operTop = (char) stack.pop(); if (operTop == '(') { stack.push(operTop); break; } else { int prec2 = 0; if (operTop == '+' || operTop == '-') { prec2 = 1; } else if (operTop == '*' || operTop == '/') { prec2 = 2; } else if (operTop == '^') { prec2 = 3; } if (prec2 < prec1) { stack.push(operTop); break; } else { output.push(operTop); } } } stack.push(ch); } }
这段代码没有优化
稍微与实际有所不同的是我贴出的实现只能计算单个数字,比如 (2+3)*6。如果需要计算更高位的数字,可以先使用 (a+b)*c ,待后缀表达式转换完成,在①处用实际数字来替换a,b,c占位符即可。
实际上关于后缀表达式的实现还有一种更简单的方式,例如:
a+b*c-(d+e)
第一步:按照运算符的优先级对所有的运算单位加括号~
式子变成拉:((a+(b*c))-(d+e))
第二步:转换前缀与后缀表达式
前缀:把运算符号移动到对应的括号前面
则变成拉:-( +(a *(bc)) +(de))
把括号去掉:-+a*bc+de 前缀式子出现
后缀:把运算符号移动到对应的括号后面
则变成拉:((a(bc)* )- (de)+ )-
把括号去掉:abc*-de+- 后缀式子出现
这里只做参考,没有实现。具体参见:http://blog.sina.com.cn/s/blog_4e0c21cc01010x38.html
3.使用JDK自带的脚本解释器。
static ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript"); public static void main(String[] args) throws ScriptException { String abc = "(1+7)%3";//"2^((4+2)/3)"; // double value = // 0; // SuffixFormula.getInstnace().formula(abc); Object value2=jse.eval(abc); System.err.println("eval:"+value2); }
需要注意的是java实现的javascript脚本解释器,只支持javascript的运算模式,像^是不支持的必须使2^2这种个就会出错,需要用Math.pow()这种javascript的函数。另外这种脚本解释器只有在JDK6以上才有,低版本是没有的。
对于简单的我推荐使用方法3.一般使用方法2.