一个符合人类自然书写习惯的数学表达式,称为中缀表达式,即四则运算
举个例子:2+(-2*3)+((4+5)-(6-7))/2= 1.0
不过中缀表达式充斥着复杂且无规律的运算优先级,是不能直接从左到右计算的,而计算器也无法像人类那样“先乘除,后加减”,更别说小括号。但人类也不能离开计算器,精密领域的带六七位小数的运算、大数计算、超多项表达式(A1 op A2 op …op An ,op∈{ + , - , * , / , ( , ) } )的运算,人力运算效率极低,没有计算器就没有现代的人类文明。
既然计算器不能从左到右计算中缀表达式,那就要设计一个算法帮助计算器计算数学表达式。目前绝大多数计算器用的是逆波兰表达式算法:将正常的中缀表达式转化为后缀表达式(逆波兰式),再对逆波兰式进行从左到右的计算。
所以科学家早就发明了计算器能够使用的算法,那还有没有算法能够替代逆波兰表达式算法呢?肯定有,当然逆波兰表达式算法是效率最优的算法了,但也存在让计算器直接计算中缀表达式的算法。
我的设想是:既然中缀表达式存在不同的优先级模块,像一条直线有了凸起,那就多次处理,把凸起的部分撸直。实际操作总结:递归消括号,循环消乘除,再循环一轮处理加减,就能计算一个简单的四则表达式(A1 op A2 op …op An ,op∈{ + , - , * , / , ( , ) } )
用Java完成了实践,欢迎出样例挑错(没做太多的测试,使用的数据在double范围内)
public class Main {
/**
* 3+(-2*3)+[(4+5)-(6-7)]/2= 2.0
* 2+(-2*3)+((4+5)-(6-7))/2= 1.0
* 1+(-2*3)+((4+5)-(6-7)]/2= error
* 1-2+3*(4+5)/6= 3.5
* 100*100/50+200*0.05-100000= -99790.0
* ((4+5)-(6-7))/2= 5.0
* -100*100+900+1100+1000/100*10*100= 2000.0
*/
public static boolean error = false;
public static double compute(String expression) {
String s = "";
double result = 0.0;
int len = expression.length();
LinkedList<String> a = new LinkedList<>();//分割并存放第一次运算的数据
//首先,将一个多项表达式字符串中的各个项分割出来,从左到右分割,依次放进链表a
for(int i=0;i<len;i++) {
if(error) {
return 0;
}
char c = expression.charAt(i);
if(c>='0'&&c<='9') {
s += c;
if(i==len-1) {
a.addLast(s);
}
}
else {
//存放运算符号以及括号()[]
if(!s.equals("")) {
if(c=='.') {
s += c;
}
else {
a.addLast(s);
s = "";
a.addLast(c+"");
}
}
else if((s.equals("")&&c=='-')) {
//记录负数
s += c;
}
else if(s.equals("")&&c!='-') {
//判断出现 (1+2) 的情况
a.addLast(c+"");
}
}
}
//错误特判,最后一个非数字的元素只能是 ) ],这个特判能排除 1. 2. 0.等情况
String last = a.getLast();
len = last.length();
if(!(last.charAt(len-1)>='0'&&last.charAt(len-1)<='9')) {
char c = last.charAt(len-1);
if(c!=')'&&c!=']') {
error = true;
return 0;
}
}
//手动为a链表添加一个结束判断符,用于处理最后一个项
a.addLast("=");
int size = a.size(),bp = 0;
LinkedList<String> b = new LinkedList<>();//存放第二次运算的数据,只存放数字与+、-
LinkedList<Character> brackets = new LinkedList<>();//存放括号的栈
int flag = 0;//记录出现第几个括号
LinkedList<String> subexpress = new LinkedList<>();//括号内子表达式
String express = "";//用于递归形参
//第一轮计算,计算括号和乘除。括号内的表达式抽取出来递归计算,所有乘除运算在本循环内完成
for(int i=0;i<size;i++) {
String as = a.get(i);
char c = as.charAt(0);//符号的字符串都只有一位,只需要截取下标0的字符即可
//如果flag为0,说明暂无记录到左括号或者左括号已被消除
if (flag==0) {
if(bp>2&&(b.get(bp-2).charAt(0)=='*'||b.get(bp-2).charAt(0)=='/')) {
//回溯处理乘除是为了适应a*(b+c)的情况
//最后一个=是为了处理边界的情况
double b2 = Double.parseDouble(b.removeLast());
char op = b.removeLast().charAt(0);
double b1 = Double.parseDouble(b.removeLast());
bp = bp-3;
if(op=='*') {
b.addLast((b1*b2)+"");
}
else {
if((b2-0)<1e-5) {
error = false;
return 0;
}
b.addLast((b1/b2)+"");
}
bp++;
}
if(i==size-1) {
break;
}
if(c=='('||c=='[') {
//准备用subexpress[i]代替express
flag++;
subexpress.addLast("");
b.addLast(express);
express = "";
brackets.addLast(c);
}
else if(c=='+'||c=='-'||c=='*'||c=='/') {
b.addLast(as);
bp++;
}
else {
b.addLast(as);
bp++;
}
}
else {
//flag>0一律进入这个部分,这个部分不进行运算,只记录子表达式
if(i==size-1) {
//如果该方法传入的多项式已遍历到= ,但仍有左括号未消除,报错并强制返回0
error = true;
return 0;
}
if(brackets.getLast()==c) {
//括号栈只能存在一种左括号,第二种左括号为子表达式的一部分
//如果遇到相同的左括号,可视为在子表达式中出现了子表达式:((a-b)*c)=
flag++;
subexpress.addLast("");//子表达式链表后新增一个空字符串记录新的子表达式
brackets.addLast(c);
}
else {
if((brackets.getLast()=='('&&c==')')||(brackets.getLast()=='['&&c==']')) {
flag--;
//( )与[ ]可以相互抵消一次
brackets.removeLast();
//左括号已经抵消,则将子表达式送入递归
if(!brackets.isEmpty()) {
//如果左括号栈不为空,说明该子表达式的值为上级子表达式的一个项
subexpress.set(flag-1,subexpress.get(flag-1)+compute(subexpress.get(flag)));
subexpress.removeLast();
}
else {
//左括号栈为空,可以将最后一个子表达式的值作为总表达式的一个项
b.set(bp,compute(subexpress.get(flag))+"");
bp++;
subexpress.removeLast();
}
}
else {
//非左、右括号,直接加入当前子表达式字符串末
subexpress.set(flag-1,subexpress.get(flag-1)+as);
}
}
}
}
//第二轮运算,计算加减,此时链表b只存在 数字字符串 以及 "+" "-",可以从左到右计算
result = Double.parseDouble(b.getFirst());
for(int i=0;i<bp;i++) {
if(i==0) {
continue;
}
char c = b.get(i).charAt(0);
if(c=='+'||c=='-') {
//经过前面的处理,正确的四则表达式最后一定是数字结尾,因此不在意越界的情况
double b1 = Double.parseDouble(b.get(++i));
if(c=='+') {
result += b1;
}
else {
result -= b1;
}
}
}
return result;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//输入类似于 1-2+3*(4+5)/6= 后面以=结束,减少误差
String expression = scanner.next();
//去除最后一个=
expression = expression.substring(0,expression.length()-1); //去掉 =
double result = compute(expression);
if(error) {
System.out.println("error");
return;
}
System.out.println(result);
}
}
小结:写完了这个算法,深刻体会了逆波兰表达式算法的强大之处,对伟大的科学家先驱抱有崇高的敬意。这个算法为了处理各种bug以及避免错误的条件(除以0、括号后接括号、小数等等),添加了不少特判,所以我觉得实用性不强,很弱。纯纯地写来玩玩,所以,放数据可以、提意见可以、友善交流可以,喷写的烂就不必了emmmmm。
这个函数只用到了一个全局变量,因此模块化做得还行,属于是cv大法即插即用,觉得能用得上的就cv吧。