本文主要讲解双栈式Java简易型科学型计算器的实现过程
所谓双栈法,就是用两个栈,一个存储操作数,另一个存储操作符,然后对表达式进行逐一扫描并根据操作符优先级对操作数栈中的数进行逻辑运算。
本程序是无GUI界面版的,下面从用户输入表达式开始讲解
将用户输入的表达式进行预处理
由于我们要实现的是科学型计算器,所以在此借鉴了一下Win10附件中科学型计算器的基本功能
下面对这些功能如何实现进行逐一解释:
**幂运算:**用户可以输入^来进行幂运算,所以可在程序内部定义一个上角标号作为一个双目运算符进行处理
**开平方根:**用户输入√进行开方运算,程序内部可以定义一个根号√单目运算符来进行处理
**开n次方根:**这里我自行定义了一个运算符“yroot”来表示,例如8 yroot 3 = 2
**阶乘:**用户可以照常输入3!这样的表示,在程序内部则把3!替换为f(3)这种形式,f是单目运算符
**三角函数:**用户正常输入sin等函数,在函数内部可取首字母s,c,t作为标识符来进行识别并运算
**反三角函数:**由于首字母都是a,故无法根据首字母区分三种反三角函数,这里采用将arctan、arccos、arcsin替换成其他不同的单个字符来进行识别并处理
**双曲函数与反双曲函数:**同上,将其转换成不同的单个字符来处理
**对数:**可将log定义为L,ln定义为l来加以区分
**取模:**将Mod取首字母标识即可
**取倒数:**无GUI版中很难让用户输入一个“望文生义”的字符来进行取倒数的运算,故将此放在GUI界面中实现
**科学计数法及其他功能:**此功能在GUI中实现
所以,本程序的核心思想就是将复杂的操作符,如arctan,log等替换为单个字符或者提取首字母来作为标识,而后在程序内部定义以这些标识符为操作符的单目或双目运算符进行运算即可。
为此,可以定义函数,将用户输入的字符串进行预处理:
public static String exchange(String temp) {
temp = temp.replace("arctanh", "b");//替换顺序不能任意更改
temp = temp.replace("arccosh", "n");
temp = temp.replace("arcsinh", "m");
temp = temp.replace("tanh", "g");
temp = temp.replace("cosh", "h");
temp = temp.replace("sinh", "j");
temp = temp.replace("arctan", "q");
temp = temp.replace("arccos", "w");
temp = temp.replace("arcsin", "r");
temp = temp.replace("Mod", "M");
temp = temp.replace("yroot", "y");
temp = temp.replace("π", Math.PI + "");
temp = temp.replace("e", Math.E + "");
String regex = "[0-9]{1,}!";//匹配诸如3!此类的表达式,并将其转换为f(3)的形式
Pattern pattern = Pattern.compile(regex);
Matcher m = pattern.matcher(temp);
Vector<String> matchRegexList = new Vector<String>();
while(m.find()){
matchRegexList.add(m.group());
}
for(int i = 0;i<matchRegexList.size();i++) {
temp = temp.replace(matchRegexList.get(i), "f(" + matchRegexList.get(i).substring(0, matchRegexList.get(i).length() - 1) + ")");
}
return temp;
}
不能替换顺序的原因:若将tanh先于arctanh替换的话,那么若有一表达式为arctanh(3),那么替换后就变成了arcg(3),程序无法识别
将预处理的表达式分裂为操作数与操作符
由于在后续计算中需要对表达式进行扫描,所以要先将预处理过的表达式进行分裂处理,例如有一表达式为33.3+44.4,进行分裂后存储在一个Vector<String>里,然后就可以对这个Vector里的字符串元素进行逐个扫描了。
Vector |
---|
33.3 |
+ |
44.4 |
public static Vector<String> split(String expression) {
Vector<String> v = new Vector<>();
StringBuffer numberString = new StringBuffer();
for (int i = 0; i < expression.length(); i++) {
if (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.' || expression.charAt(i) == 'E')//捕获数字
numberString.append(expression.charAt(i));//由于数字可能由多个字符组成,所以这里先暂存
else if((expression.charAt(i) + "").matches("t{0,1}c{0,1}s{0,1}L{0,1}a{0,1}")) {//捕获一些操作符,下同
v.add(expression.charAt(i) + "");
i += 2;//捕获后需要将i加上对应的值以便对下一字符扫描,例如abs,扫描到a进此分支,此时i位于a的位置,加2后到s的位置
}
else if((expression.charAt(i) + "").matches("l{0,1}")) {
v.add(expression.charAt(i) + "");
i += 1;
}
else if((expression.charAt(i) + "").matches("q{0,1}w{0,1}r{0,1}f{0,1}√{0,1}b{0,1}n{0,1}m{0,1}g{0,1}h{0,1}j{0,1}")) {
v.add(expression.charAt(i) + "");
i += 0;
}
else {
if (numberString.length() > 0) {//若有暂存的操作数,则加入Vector
v.add(numberString.toString());
numberString.setLength(0);
}
if (!Character.isSpaceChar(expression.charAt(i))) {//若非空格,则加入
v.add(expression.charAt(i) + "");
}
}
}
if (numberString.length() > 0)//检查是否还有暂存的操作数
v.add(numberString.toString());
return v;
}
逐元素扫描已分裂的表达式并利用双栈进行计算
public static double evaluateExpression(String expression) {
expression = exchange(expression);
Stack<Double> operandStack = new Stack<>();
Stack<Character> operatorStack = new Stack<>();
Vector<String> tokens = split(expression);
for (int i = 0; i < tokens.size(); i++) {
if (tokens.get(i).charAt(0) == '+' || tokens.get(i).charAt(0) == '-') {
while (!operatorStack.empty() && (operatorStack.peek() + "").matches("\\+{0,1}-{0,1}\\*{0,1}/{0,1}c{0,1}s{0,1}t{0,1}L{0,1}%{0,1}l{0,1}q{0,1}w{0,1}r{0,1}M{0,1}f{0,1}√{0,1}y{0,1}b{0,1}n{0,1}m{0,1}g{0,1}h{0,1}j{0,1}a{0,1}"))
processAnOperator(operandStack, operatorStack);
operatorStack.push(tokens.get(i).charAt(0));
}
else if (tokens.get(i).charAt(0) == '*' || tokens.get(i).charAt(0) == '/' || tokens.get(i).charAt(0) == 'M' || tokens.get(i).charAt(0) == '%') {
while (!operatorStack.empty() && (operatorStack.peek() + "").matches("\\*{0,1}/{0,1}c{0,1}s{0,1}%{0,1}t{0,1}L{0,1}l{0,1}a{0,1}M{0,1}q{0,1}w{0,1}r{0,1}f{0,1}√{0,1}y{0,1}b{0,1}n{0,1}m{0,1}g{0,1}h{0,1}j{0,1}a{0,1}"))
processAnOperator(operandStack, operatorStack);
operatorStack.push(tokens.get(i).charAt(0));
}
else if(tokens.get(i).charAt(0) == '^') {
while(!operatorStack.empty() && (operatorStack.peek() == '^' || operatorStack.peek() == 'y'))
processAnOperator(operandStack, operatorStack);
operatorStack.push(tokens.get(i).charAt(0));
}
else if(tokens.get(i).charAt(0) == 'y') {
while(!operatorStack.empty() && operatorStack.peek() == 'y')
processAnOperator(operandStack, operatorStack);
operatorStack.push(tokens.get(i).charAt(0));
}
else if(tokens.get(i).matches("t{0,1}c{0,1}s{0,1}L{0,1}l{0,1}q{0,1}w{0,1}r{0,1}f{0,1}√{0,1}b{0,1}n{0,1}m{0,1}g{0,1}h{0,1}j{0,1}a{0,1}")) {
operatorStack.push(tokens.get(i).charAt(0));
}
else if (tokens.get(i).charAt(0) == '(')
operatorStack.push(tokens.get(i).charAt(0));
else if (tokens.get(i).charAt(0) == ')') {
while (operatorStack.peek() != '(')
processAnOperator(operandStack, operatorStack);
operatorStack.pop();
}
else {
operandStack.push(Double.valueOf(tokens.get(i)));
}
}
while (!operatorStack.empty())
processAnOperator(operandStack, operatorStack);
return operandStack.pop();
}
这里用token存储分裂完的表达式,并开了两个栈,用于存储操作符和操作数。
接下来就对token中的每一个元素进行逐一扫描,并将元素加入栈中,规则如下:
-
如遇到同优先级的双目操作符,则检查操作符栈是否为空。若为空则直接将该操作符压入操作符栈;若不为空,则检查操作符栈栈顶的操作符的优先级是否大于或等于扫描到的操作符。若不满足条件,则直接将操作符压入操作符栈;若满足条件,则调用processAnOperator函数进行计算。
-
如遇到单目操作符,直接压入操作符栈即可。
-
如遇到左括号,直接压入操作符栈。
-
如遇到右括号,开始处理括号内的表达式。若栈顶操作符不为左括号,则循环调用processAnOperator直到栈顶操作符为左括号。然后操作符弹栈,弹出左括号,括号内表达式运算完毕。
-
如遇操作数,则直接压入操作数栈即可。
最后检查一下操作符栈中是否为空。若不为空,则循环调用processAnOperator直到栈空为止。
processAnOperator
public static void processAnOperator(Stack<Double> operandStack, Stack<Character> operatorStack) {
char op = operatorStack.pop();
double op1 = 0, op2 = 0;
if(op == '+' || op == '-' || op == '*' || op == '/' || op == 'M' || op == '^' || op == 'y' || op == '%') {
op1 = operandStack.pop();
op2 = operandStack.pop();
}
else if((op + "").matches("t{0,1}c{0,1}s{0,1}L{0,1}l{0,1}q{0,1}w{0,1}r{0,1}f{0,1}√{0,1}b{0,1}n{0,1}m{0,1}g{0,1}h{0,1}j{0,1}a{0,1}")) {
op1 = operandStack.pop();
}
if (op == '+')
operandStack.push(op2 + op1);
else if (op == '-')
operandStack.push(op2 - op1);
else if (op == '/')
operandStack.push(op2 / op1);
else if (op == '*')
operandStack.push(op2 * op1);
else if(op == 'M' || op == '%')
operandStack.push(op2 % op1);
else if(op == '^')
operandStack.push(Math.pow(op2, op1));
else if(op == 'y')
operandStack.push(Math.pow(op2, 1 / op1));
else if(op == 't')
operandStack.push(!DEG ? Math.tan(op1) : Math.tan(Math.PI * op1 / 180));
else if(op == 's')
operandStack.push(!DEG ? Math.sin(op1) : Math.sin(Math.PI * op1 / 180));
else if(op == 'c')
operandStack.push(!DEG ? Math.cos(op1) : Math.cos(Math.PI * op1 / 180));
else if(op == 'L')
operandStack.push(Math.log10(op1));
else if(op == 'l')
operandStack.push(Math.log(op1));
else if(op == 'q')
operandStack.push(!DEG ? Math.atan(op1) : Math.atan(op1) * 180 / Math.PI);
else if(op == 'w')
operandStack.push(!DEG ? Math.acos(op1) : Math.acos(op1) * 180 / Math.PI);
else if(op == 'r')
operandStack.push(!DEG ? Math.asin(op1) : Math.asin(op1) * 180 / Math.PI);
else if(op == 'f')
operandStack.push(fact(Math.round(op1)));
else if(op == '√')
operandStack.push(Math.sqrt(op1));
else if(op == 'b')
operandStack.push(0.5 * Math.log((1 + op1) / (1 - op1)));
else if(op == 'n')
operandStack.push(Math.log(op1 + Math.sqrt(op1 * op1 - 1)));
else if(op == 'm')
operandStack.push(Math.log(op1 + Math.sqrt(op1 * op1 + 1)));
else if(op == 'g')
operandStack.push(Math.tanh(op1));
else if(op == 'h')
operandStack.push(Math.cosh(op1));
else if(op == 'j')
operandStack.push(Math.sinh(op1));
else if(op == 'a')
operandStack.push(Math.abs(op1));
else {
}
}
核心思想就是先将操作符栈弹出一个操作符,判断这是单目还是双目操作符,相应的操作数栈弹出一个或两个操作数。然后根据操作符这个标识符进行判断并计算即可。