一、前言
最近在开发中遇到一个场景,用户输入一个字符串,这个字符串是一个数学计算式,让求出计算的结果
支持
- 四则运算
- 多位小数、整数
- 大中小括号
久闻大佬们经常提起,可以利用栈将一个字符串形式的数学表达式,计算得出最终的结果,于是乎,小编利用自己的一把头发,终于将其实现了出来。
二、问题
计算器这个东西,相信不管是女士们,先生们,老人,孩子,还是叔叔大爷应该都有听过,都见过,都用过,大家可能会有下面这些问题:
问:计算器,大家都用过啊,1+1=2,继续在开发中,sum=number1+number,每天都在用啊。
答:没错,就是常说的最简单的计算器,只不过您提到的sum=number1+number,其中的三个变量都是有自己的类型的,本文讲的是表达式完全是一个字符串。
问:都是字符串那也简单啊,字符串拆解,然后挨个计算呗!
答:没错,的确是需要字符串拆解。但是数学计算中,运算符是有优先级的,例如乘法比加法优先级高,而且每个数字可能由1,2,3,4…等多位数组成,还可以有小数点,那如何挨个算?
问:
答:好的,接下来,就和大家一起学习,如何用栈实现计算器
三、餐前开胃
如果您对栈还不是很了解,可以看一看笔者之前的博客【数据结构】不看会后悔版本的单链表实现栈 ,了解一下栈,以及如何通过单链表实现栈。
四、源码地址
伙伴们,吃水不忘挖井人,大家start点起来
https://gitee.com/sunshineAndDream/data-structures-arithmetic.git
五、思路:
- 由于拿到的是用户输入的一个字符串,所以首先,我们应该先拿到字符串的每一个元素
- 需要准备两个栈,一个存放数字,一个存放运算符号
- 取出字符串中的数字和符号,分别放入到对应的栈
1、数字栈需要注意的问题
当读取到一个字符是数字时,不能立刻压入栈中,因为可能是一个多位整数,例如:11,222,3333,也可能是一个小数,例如:3.14,所以我们需要继续读,直到下一位不是0到9的数字,且不是小数点为止。
2、符号栈需要注意的问题
- 当读取到一个字符是符号时,需要看一下栈中是否有符号
- 如果符号栈中没有元素或者当前符号是左半括号:({、[、() ,则直接将符号放入到栈中
- 否则:
- 如果当前符号是右半括号:(}、]、)) ,则循环出栈,并计算数值,直到找到当前括号的另一半
- 然后判断当前运算符和现在栈顶的运算符的优先级
- 如果当前的优先级小于等于符号栈栈顶的优先级,则需要取出符号栈的一个元素,数字栈的两个元素进行运算
- 如果此时是减法或者除法,需要让第二个出栈的元素减去(除以)第一个出栈的元素。
- 如果当前的优先级大于符号栈栈顶符号的优先级,则直接压入栈
解析:
- 如果当前的优先级小于等于符号栈栈顶的优先级,则需要取出符号栈的一个元素,数字栈的两个元素进行运算
问:为什么是小于等于需要计算,而大于直接入栈?
答:在数学中,是从左往右计算,但是存入到栈中,顺序恰好相反;因为将所有的数据都入栈结束以后,会将所有元素挨个出栈计算:
- 如果当前符号的优先级大于前一个优先级,那么因为后进先出,所以计算优先级高,也是先计算、
- 如果当前符号的优先级小于等于前一个优先级,那么因为后进先出,并且数学计算是从左到右,所以需要提前计算,否则不符合数学计算的规则
问:为什么减法(除法)需要让第二个出栈的元素减去(除以)第一出栈的元素?
答:因为栈是后进先出,是从表达式头部开始入栈,例如表达式:3-2,数字栈中,先入栈3,在入栈2,此时顺序出现了颠倒,所有需要颠倒一下。因为乘法和出发
问:为什么加法(乘法)没有影响
答:因为加法(乘法)两个元素,交换位置结果不变。
六、流程图
七、核心代码
如果需要全部代码,欢迎去gitee上拉
/**
* 计算器类
*/
class CalculatorString{
//定义数字的栈
private ArrayStack<Double> numberStack = new ArrayStack<>(100);
private ArrayStack<Character> operationStack = new ArrayStack<>(100);
public void calculator(String expression){
//region 一、变量定义
//定义字符串索引
int index = 0;
//定义字符串重组,用来解决多位数问题
StringBuilder keepString = new StringBuilder();
//endregion
//region 二、核心逻辑
//遍历需要计算的字符串
while (index < expression.length()){
if (index == 25){
System.out.println("开始了");
}
System.out.println(index);
//获取到需要计算字符串的第index(默认从0开始)个字符
char c = expression.substring(index, index + 1).charAt(0);
//如果是数字
if(CalculatorUtils.isNumber(c)){
//如果已经遍历到最后一位,则直接加入到栈
if(index == expression.length()-1){
numberStack.push((double) (c-48));
} else {
//如果不是是最后一位
//先将这位数字添加到keepString字符串中
keepString.append(c);
//获取到index的下一位
c = expression.substring(index + 1, index + 2).charAt(0);
//如果c是数字(此时的c已经是第二位数字),则证明此时的数字不是个位数,继续遍历
while (CalculatorUtils.isNumber(c)){
//将第二位数字添加到其中
keepString.append(c);
//索引移动
index++;
//获取到下一位,然后继续判断你
if(index == expression.length()-1){
break;
}
c = expression.substring(index + 1, index + 2).charAt(0);
}
//运行到这里,即下一位肯定不是数字,即已经读取完改数
numberStack.push(Double.parseDouble(keepString.toString()));
//清空keepString
keepString.delete(0,keepString.length());
}
} else if(CalculatorUtils.isOper(c)) {
//如果是符号
//1.如果栈中是空的或者是左侧符号,则直接加入到栈
if(operationStack.isEmpty() || CalculatorUtils.isLeftBracket(c)){
operationStack.push(c);
} else {
//2.如果是右侧括号,则需要计算
if(CalculatorUtils.isRightBracket(c)){
//看一看当前符号栈中的内容,是看一看,不是出栈
Character lastOperation = operationStack.peek();
//直到找到和自己同级的符号
while (CalculatorUtils.bracketEquals(lastOperation) != CalculatorUtils.bracketEquals(c)){
//调用方法,出栈两个数,一个符号,结算出结果,并将结果压入栈中
numberStack.push(popAndCalculator());
//移动到下一个
lastOperation = operationStack.peek();
}
//因为上面是一直出栈,直到两个符号同级别的时候结束while,但是此时的同级别符号已经出栈
Character peek = operationStack.pop();
}
//3.如果栈不是空的,且不是括号,取出元素,判断和当前字符的优先级
//现在的运算符级别
//之前的运算符级别
//如果现在的小于等于之前的,则计算
else if(CalculatorUtils.priority(c) <= CalculatorUtils.priority(operationStack.peek())){
double cal = popAndCalculator();
numberStack.push(cal);
operationStack.push(c);
} else {
operationStack.push(c);
}
}
}
index ++ ;
}
//endregion
System.out.println("开始计算");
//region 三、最后计算
while (true){
if(numberStack.getTop() == 0 ){
System.out.println(numberStack.pop());
break;
}
double lastNumber = numberStack.pop();
double beforeLastNumber = numberStack.pop();
Character lastOperation = operationStack.pop();
double cal = CalculatorUtils.cal(lastNumber, beforeLastNumber, lastOperation);
numberStack.push(cal);
}
//endregion
}
/**
* 出栈两个符号元素,并计算
* @return
*/
private double popAndCalculator(){
double lastNumber = numberStack.pop();
double beforLastNumber = numberStack.pop();
double cal = CalculatorUtils.cal(lastNumber, beforLastNumber, operationStack.pop());
return cal;
}
}
八、总结
- 通过对实例的应用,发现原来人尽皆知的栈,竟然会有这么牛的用法。
- 通过这个例子,应该就是windows底层对计算器实现的一个简易版本。
- 其实这个例子是对学习资料的一个总结,只不过小编觉得老师实现的代码有部分缺陷,所以笔者对其进行了修改。
- 程序及人生,突然发现数据结构也挺好玩的!!!
好了,又要说再见的时间了,今天的内容,你学废了吗?