在公司实习,老大给留了个练习:
要求实现一个简易计算器(不需要界面)
能进行四则运算和括号运算(四则运算倒是好搞,但是加括号有点难度)
于是就在网上参考了几篇(下面代码是参考一位大佬的,但是忘了那篇博客的链接,如果大佬认出来了请联系我下)
几乎都是用栈实现的中缀表达式转换成后缀表达式
但是在上面大佬的代码中,老大给我指出了几个问题:
1.不能进行负号运算
2.大数据运算结果为科学计数法
3.小数运算有问题
所以在之前的基础上,我又做了一些处理,下面上代码:
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.Stack;
/**
*
* @版权 : Copyright (c) 2017-2018 *********公司技术开发部
* @author: gaozhenchao
* @E-mail: 1226046769@qq.com
* @版本: 1.0
* @创建日期: 2019年1月14日 上午9:19:49
* @ClassName CalculateUtil
* @类描述-Description: TODO(这里用一句话描述这个方法的作用)
* @修改记录:
* @版本: 1.0
*/
public class SimpleCalcularor {
// 操作数栈
private static Stack Operands;
// 操作符栈
private static Stack Operators;
// 操作符集合
private static final Set C_OperatorSet = new HashSet() {
/**
*
*/
private static final long serialVersionUID = 1L;
{
add('+');
add('-');
add('*');
add('/');
add('(');
add(')');
}
};
// 获取优先级
private static int getOperatorPriority(char ch) {
if (ch == '+' || ch == '-')
return 0;
else if (ch == '*' || ch == '/')
return 1;
else
return -1;
}
// 中缀转后缀
private static String infixToSuffix(String expression) throws Exception {
Operators = new Stack<>();
Operators.clear();
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < expression.length(); i++) {
char ch = expression.charAt(i);
if (ch == ' ')
continue;
if (C_OperatorSet.contains(ch)) {
// 如果操作符栈为空
if (Operators.empty()) {
if (ch == ')') { // 只要不是 ) 就入栈
throw new Exception("表达式错误");
// System.out.println("括号不匹配");
// return sBuilder.toString();
}
Operators.push(ch);
} else if (ch == '(') {
Operators.push(ch);
} else if (ch == ')') { // 如果是 ) 括号
char top;
while ((top = (char) Operators.peek()) != '(') { // 如果当前操作符栈的第一个不是(
if (Operators.empty()) {
// System.out.println("括号不匹配");
// return sBuilder.toString();
throw new Exception("表达式错误");
}
// 把运算符加入sBuilder
sBuilder.append(top);
Operators.pop();
}
Operators.pop();
} else { // 如果是运算符
char top = (char) Operators.peek();
if (getOperatorPriority(ch) <= getOperatorPriority(top)) { // 如果优先级小于操作符栈中第一个
while (!Operators.empty()
&& getOperatorPriority(ch) <= getOperatorPriority(top = (char) Operators.peek())) {
// 把运算符加入sBuilder
sBuilder.append(top);
Operators.pop();
}
}
Operators.push(ch);
}
} else { // 如果是数字 加入sBuilder(把[3]的括号去除)
sBuilder.append("[" + ch);
while (i + 1 < expression.length()
&& (((ch = expression.charAt(i + 1)) == '.') || (ch >= '0' && ch <= '9'))) {
sBuilder.append(ch);
++i;
}
sBuilder.append(']');
}
}
while (!Operators.empty()) {
if ((char) Operators.peek() == '(') {
throw new Exception("表达式错误");
// System.out.println("括号不匹配");
// return "";
}
sBuilder.append(Operators.peek());
Operators.pop();
}
return sBuilder.toString();
}
// 处理负号
public static String HandleMinus(String expression) {
for (int i = 0; i < expression.length(); i++) {
char ch = expression.charAt(i);
// 存储负号个数
int count = 0;
// 遍历表达式,如果遍历到负号
if (ch == '-') {
// 存储变量加一
count++;
// 如果负号在第一位
if (i == 0 && expression.charAt(i + 1) >= '0' && expression.charAt(i + 1) <= '9') {
// 直接在前面加0
expression = "0" + expression;
i = 0;
continue;
// 如果负号前面是括号(说明负号在表达式中) 采用的办法是把0插入到负号之前 5*(-3) -> 5*(0-3)
} else if (expression.charAt(i - 1) == '(' && expression.charAt(i + 1) >= '0'
&& expression.charAt(i + 1) <= '9') {
// 新建一个表达式长度加负号个数长度的数组(扫描一个负号添加一个0)
char[] c = new char[expression.length() + count];
// 给新的字节数组赋值
for (int n = 0; n < expression.length(); n++) {
c[n] = expression.charAt(n);
}
// 把以负号索引开始的数据右移
for (int j = c.length - 2; j > i - 1; j--) {
c[j + 1] = c[j];
}
// 赋值到i位置
c[i] = '0';
expression = String.valueOf(c);
// 继续向下扫描
i++;
continue;
}
}
}
return expression;
}
public static BigDecimal evalExp(String expression) throws Exception {
Operands = new Stack<>();
Operands.clear();
// double用二进制计算,因为小数点转换成二进制运算有精度损失,而且大数据运算会转换成科学计数法,所以采用decimal
BigDecimal ret = null;
String suffix = infixToSuffix(expression);
System.out.println("后缀表达式为: " + suffix);
for (int i = 0; i < suffix.length(); i++) {
if (suffix.charAt(i) == '[') {
i++;
int beginIndex = i, endIndex = i;
while (']' != suffix.charAt(i)) {
i++;
endIndex++;
}
// 取两个括号中间的数字
Operands.push(BigDecimal.valueOf(Double.valueOf(suffix.substring(beginIndex, endIndex))));
} else {
BigDecimal left, right;
BigDecimal res = null;
right = (BigDecimal) Operands.peek();
Operands.pop();
left = (BigDecimal) Operands.peek();
Operands.pop();
switch (suffix.charAt(i)) {
case '+':
res = left.add(right);
break;
case '-':
res = left.subtract(right);
break;
case '*':
res = left.multiply(right);
break;
case '/':
res = left.divide(right);
break;
}
Operands.push(res);
}
}
if (Operands.size() > 1) {
throw new Exception("表达式错误");
}
ret = (BigDecimal) Operands.peek();
Operands.pop();
return ret;
}
// (3+4)*5 10000000*1000000 0.65656544*3
public static void main(String[] args) throws Exception {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个含+-*/()的表达式,请确认输入合法");
// 5*(-3) -> 5*(0-3)
String expression = input.nextLine();
// 处理负号
expression = HandleMinus(expression);
// 去除BigDecimal后面的无用0
System.out.println(expression + " =" + evalExp(expression).stripTrailingZeros().toPlainString());
input.close();
}
}
首先说一下小数运算问题:在之前的代码中,转换后缀表达式后,接下来要进行计算,但是两个操作数的数据类型为double。
但是如果小数超出double范围,就会出现精度丢失的情况
然后是大数据计算的问题:数值较大的两个数计算时,产生的结果是用科学计数法显示的,但是日常中我们想看到的是正常的显示结果。产生这个的原因是:浮点数(float,double)整数部分达到8位及以上,会以科学计数法显示。
解决的办法有两种:
// 方法一:NumberFormat
private static String big(double d) {
NumberFormat nf = NumberFormat.getInstance();
// 是否以逗号隔开, 默认true以逗号隔开,如[123,456,789.128]
nf.setGroupingUsed(false);
// 结果未做任何处理
return nf.format(d);
}
//方法二: BigDecimal
private static String big2(double d) {
BigDecimal d1 = new BigDecimal(Double.toString(d));
BigDecimal d2 = new BigDecimal(Integer.toString(1));
// 四舍五入,保留2位小数
return d1.divide(d2,2,BigDecimal.ROUND_HALF_UP).toString();
}
因为后面还要对结果进行处理,所以这里采用第二种。
BigDecimal的小数计算结果后面会带0,所以用以下方式再次进行处理
(BigDecimal的结果).stripTrailingZeros().toPlainString()
最后是处理负号的问题:基本步骤都写在代码注释里面了,可以作为参考
以上就是对表达式计算器的改进,如果有不对的地方,欢迎大佬前来指正。(由于某种原因并没有对大量的数据进行测试)
更新:由于老大让我优化代码,但是前面的逻辑动不了(其实是我想不出来),所以把目标放在了处理负号的函数上。
具体处理如下:
1.首先注释掉原处理函数
2.作为代替,现采用正则表达式处理(只需要一行就可以代替上面几十行的函数)
具体解释如下:
(?=exp) | 匹配exp前面的位置 |
(?<!exp) | 匹配前面不是exp的位置 |
连起来就是:匹配前面不是0-9 ) } ] 的位置
匹配 - 0-9 ( { [ 前面的位置
上代码:
public static void main(String[] args) throws Exception {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个含+-*/()的表达式,请确认输入合法");
// 5*(-3) -> 5*(0-3)
String expression = input.nextLine();
// 处理负号
// expression = HandleMinus(expression);
// 匹配前面不是0-9 ) } ] 的位置 &&&&&匹配- 0-9 ( { [ 前面的位置
// 注意-开头的地方前面一定不能是数字或者反括号,如9-0,(3-4)-5,这里地方是不能加0的
// 它的后面可以是数字或者正括号,如-9=>0-9, -(3*3)=>0-(3*3)
expression = expression.replaceAll("(?<![0-9)}])(?=-[0-9({])", "0");
// 去除BigDecimal后面的无用0
System.out.println(expression + " =" + evalExp(expression).stripTrailingZeros().toPlainString());
input.close();
}
就是这样,省去了很多步骤