本文基于尚硅谷韩顺平老师的《Java数据结构与算法》--栈实现综合计算器 并加以改进
注意点:
支持运算符 + - * / ( ),负数开头
使用StringTokenizer分割字符串
不支持小数
不允许出现'()'这样的空括号
不予许空格(这个加一个过滤其实也能可以,但本文的核心是如何处理括号)
思路分析及主要部分
运算思路分析
栈:实现运行优先级,初始化一个numStack大小为3,一个opeStack大小为2
利用StringTokenizer将数字与运算符分开(开头的负数就要另外处理)
数字放入numStack,运算符放入opeStack
当token为数时,直接入栈。
当token为运算符时,先判断opeStack内是否为空,为空就直接入栈,否则判断栈内运算符优先级是否大于等于当前元素优先级:若当前元素大于,直接入栈,否则先计算栈内运算符
计算操作:从opeStack中弹出一个运算符,从numStack中弹出两个运算数,调用用于计算的方法,得到运算结果。将运算结果压入numStack
注意:opeStack内元素优先级是大于等于就应该触发先运算操作!
大于好理解,级别高先运算嘛。等于是因为表达式应从左到右运算,先入栈的应该先运算。
先计算栈内运算符的判断应该是一个循环。因为栈内可能不止存放一个运算符,从栈中弹出已存放的运算符,当前运算符依然要注意与栈内元素比较,不要着急入栈!
为什么numStack大小为3,一个opeStack大小为2即可?
因为将当前运算符能否入栈设计为一个循环,只要已存运算符优先级大于等于当前运算符优先级,就先弹出计算,导致opeStack栈内不能存放相同优先级的运算符,故最多就两个,因为就两种优先级。并且如果是两个,一定是底大顶小。
体会一下:2 - 2x3 - 1 != 2 - (2x3 - 1)
实现运算的代码
if (isOperate(s)) {
//操作符只有一位
int ope = s.charAt(0);
//操作符
//里面有符号就判断优先级,保证符号栈中最多两个符号(顶大低小)
//符号栈空就直接入栈(下面),否则再判断priority(opeStack.peek())可能空指针
while (!opeStack.isEmpty() && priority(ope) <= priority(opeStack.peek())) {
//6 - 2 - 1没问题
//6 - 2*3 -1就有问题,当-1进入时,会成为6-6-1压入栈,下次进入会计算6-(6-1)
//故不能立即压入栈
//小于等于栈顶优先级就先计算前面的
num1 = numStack.pop();
num2 = numStack.pop();
operate = opeStack.pop();
result = cal(num1, num2, operate);
numStack.push(result);
}
//操作完opeStack前面的所有操作符,现在的操作符一定是最大的,故将当前符号入栈
opeStack.push(ope);
}
处理括号思路分析
+ 括号优先级最高,碰到括号可以先计算括号内的内容
+ 括号内的内容也是表达式,获取到完整的一对括号内容,可以利用递归处理括号内的运算,返回运算结果压入当前栈的numStack。 理解递归也是学习栈的一部分!
+ 处理嵌套括号思路:
如同(a+b+(c+d)+f),外层括号'('会与 内层括号') '匹配得到 a+b+(c+d ,而不是我们认为的外层括号的所有内容。可以通过检测目前得到的内部内容in,分析in内是否再此出现'( '推断出是否外层括号与内层括号相匹配并且可能内层不止嵌套一对括号,可能多对,这就需要分析有多少对'('去推断相应有多少')'但又不能单纯地计算目前得到的内部内容in有多少个'(',因为在找')'时扩展可能又加入了'('如:((a+b)+(c+d)+f),依靠获取'('括号的个数的方法会得到((a+b)+(c+d),此时是左3右2 (最外层的用于判断没加入in,也没必要)。为解决此问题,可以引入一个指针index记录'('的位置,下次从index后寻找是否还有'('
理解左右匹配数量守恒!
实现代码:
if (s.matches("\\(")) {
String in = st.nextToken(")");
//右括号要加上去,否则下次调用token = st.nextToken(")")得到的是上次剩下来的),而不是向后扩展的)
//就算)其实是不做任何处理的,但不能忽视调这个,否则调用nextToken得到的可能是)而不是运算数
if (in.equals(")")) {
//先来一个左括号,再来一个右括号就完了,说明是一对空括号
throw new RuntimeException("请勿输入空括号!");
}
in += st.nextToken();
int index = -1;
/*
如同(a+b+(c+d)+f),外层括号( 会与 内层括号) 匹配得到 a+b+(c+d 而不是我们认为的外层括号的所有内容
可以通过检测目前得到的内部内容in,分析in内是否再此出现( 推断出是否外层括号与内层括号相匹配
而且可能内层不止嵌套一对括号,可能多对,这就需要分析有多少对( 去推断相应有多少)
但又不能单纯地计算目前得到的内部内容in有多少个(,因为可能在找)时扩展又加入了(
如:((a+b)+(c+d)+f),依靠获取(括号的个数的方法会得到((a+b)+(c+d),此时是左3右2(最外层的用于判断没加入in,也没必要)
为解决此问题引入一个指针index记录(的位置,下次从index后寻找是否还有(
*/
//处理嵌套括号:用循环直到获取到最右边的右括号
while ((index = in.indexOf("(", index + 1)) != -1) {
//比如(a+b)
//相当于token = st.nextToken(")"),为填充括号内的东西,这里就是a+b
//may可能是内容,也可能是)
String may = st.nextToken();
if (may.equals(")")) {
//若是),就说明前面就把填充括号内的内容的活干完了,就只走一步:补右括号
in += may;
} else {
//填充内容
in += may;
//为了补上),这里就是a+b)
in += st.nextToken();
}
}
int resIn = compute(in);
//这部不要忘了,压入当前栈,而不仅是递归内
numStack.push(resIn);
}
处理嵌套括号的思考
/*
但是,要是括号内没有内容,或已经要结束了 (( )“就是这里是空的”),在填充时就把右括号补上了,那再补右括号就会出错
比如((1) ),第一个括号进行填充第一对括号的内容时,填充到了 '('(1 再补上右括号),即(1)
此时发现填充内容内有第二对括号(嵌套括号),于是,按同一个逻辑,接下来是填充第二对括号内的内容
而st的指针已经指到括号外了,即实际上做的事不是我们想的那样,它不在填充括号内的内容,而是在进行外部扩展!!
导致bug的原因是我们想的逻辑与代码实现的逻辑不同:在填充外部括号内容时,已经把内部括号的内容填充完了
故接下来要做的事就是找到外部括号本应配对的右括号(先前是外部左括号与内部右括号匹配了)
而在这个过程中,可能((a+b)+c),即外部匹配一次不一定能把外部括号内容填充完
但也不能单纯地判断下次填充内容为),就认为外部内容填充完了
可能有( ((a+b)+c) + d),或f+(b-(a*(c+d))+e)
故我的想法是若st.nextToken()的填充物不是),就走两步,一填充二补右括号
但若是),就说明前面就把填充括号内的内容的活干完了,就只走一步:补右括号
不要担心))+a))的问题,左括号没一次填充完的次数与右边需要走两步的次数一定是相等的
在走两步进行填充时,可能是在帮其他的括号填充,但填充次数一定是守恒的!
*/
整体实现代码
注:第46,47行使用的是笔者自己用链表实现的Stack,读者想要测试该代码将其改为Java自带的Stack即可
46 LinkedListStack<Integer> numStack = new LinkedListStack<>(3);
47 LinkedListStack<Integer> opeStack = new LinkedListStack<>(2);
import java.util.StringTokenizer;
/**
* Description: 设计中缀表达式整型计算器,表达式不能有空括号,不能有空格
*
* @Project: DataStructures
* @Package: my.learn.stack
* @Author: Yang Xiong Email:2829727259@qq.com
* @Version: jdk1.8.0_131
* @Time 2023/2/2 - 15:55
*/
public class Calculator {
public static void main(String[] args) {
// String exp = "-1-2*3-3*3";//-16
// String exp = "-1-2*3-3";
// String exp = "-13-2*30-11+10+20*5-2*8-1";//9
// String exp = "(3-1)+2";
// String exp = "((3-1)+2)";
// String exp = "-3";
// String exp = "(-3)";
// String exp = "(-3)";
// String exp = "()";
// String exp = "";
// String exp = "((3-1)+(-2*(-3)))";
// String exp = "2*(-3)*(-3)*()+(1)";
// String exp = "(1+2)+(-3)+(((-3+2))*2)";
// String exp = "(1+2)+2+3+(())";
// String exp = "(-1+2*(-3+((-3+4/2)+2*3)))";//3
// String exp = "((2)+(2)*(4))/2";//5
// String exp = "(12/(2)+(2)*(4))/2";//7
String exp = "(12/(2)+(2)*(4))/2";
int res = Calculator.compute(exp);
System.out.println("res = " + res);
}
public Calculator() {
}
public static int compute(String exp) {
LinkedListStack<Integer> numStack = new LinkedListStack<>(3);
LinkedListStack<Integer> opeStack = new LinkedListStack<>(2);
String regex = "[+\\-*/()]";
//操作符的正则表达式
StringTokenizer st = new StringTokenizer(exp, regex, true);
//必要n-1个符号 和 n个操作数,故总token是2n -1个
//操作数过多,为以防万一,报错
//实际上最多只会存3个操作数,两个操作符
/*if (((st.countTokens() + 1) / 2) > 10) {
//最多10个操作数
//当然也可以n=(st.countTokens() + 1) / 2
// 然后new一个大小为n的栈,但,这都是最坏打算,实际不一定用的到
throw new RuntimeException("表达式过于复杂,请减少操作数");
}*/
int result = 1000000000;
int num1;
int num2;
int operate;
//专门处理第一个数为负数的情况,因为StringTokenizer无法获得-和数字一起的token,即使我们利用正则表达式使匹配开头不为-(原因不详)
String temp = null;
try {
temp = st.nextToken();
} catch (Exception e) {
throw new RuntimeException("请输入数据!");
}
if (temp.charAt(0) == '-') {
numStack.push(-Integer.parseInt(st.nextToken()));
} else if (temp.matches("-?\\d+")) {//匹配整型
numStack.push(Integer.parseInt(temp));
} else if (temp.matches("\\(")) {
//可能开头就是括号(
String in = st.nextToken(")");
if (in.equals(")")) {
//先来一个左括号,再来一个右括号,说明是一对空括号
throw new RuntimeException("请勿输入空括号!");
}
//右括号要加上去,否则下次调用token = st.nextToken(")")得到的是上次剩下来的),而不是向后扩展的)
//就算)其实是不做任何处理的,但不能忽视调这个,否则调用nextToken得到的是)而不是运算数
in += st.nextToken();
int index = -1;
while ((index = in.indexOf("(", index + 1)) != -1) {
//比如(a+b)
//相当于may = st.nextToken(")"),为填充括号内的东西,这里就是a+b
//may可能是内容,也可能是)
String may = st.nextToken();
if (may.equals(")")) {
//若是),就说明前面就把填充括号内的内容的活干完了,就只走一步:补右括号
in += may;
} else {
//填充内容
in += may;
//为了补上),这里就是a+b)
in += st.nextToken();
}
}
int resIn = compute(in);
numStack.push(resIn);
}
while (st.hasMoreTokens()) {
//注意nextToken要重新设置分隔符
String s = st.nextToken(regex);
if (isDigit(s)) {
//数字直接入数字栈
numStack.push(Integer.parseInt(s));
} else if (isOperate(s)) {
//操作符只有一位
int ope = s.charAt(0);
//操作符
//里面有符号就判断优先级,保证符号栈中最多两个符号(顶大低小)
//符号栈空就直接入栈(下面),否则再判断priority(opeStack.peek())可能空指针
while (!opeStack.isEmpty() && priority(ope) <= priority(opeStack.peek())) {
//6 - 2 - 1没问题
//6 - 2*3 -1就有问题,当-1进入时,会成为6-6-1压入栈,下次进入会计算6-(6-1)
//故不能立即压入栈
//小于等于栈顶优先级就先计算前面的
num1 = numStack.pop();
num2 = numStack.pop();
operate = opeStack.pop();
result = cal(num1, num2, operate);
numStack.push(result);
}
//操作完opeStack前面的所有操作符,现在的操作符一定是最大的,故将当前符号入栈
opeStack.push(ope);
} else if (s.matches("\\(")) {
String in = st.nextToken(")");
//右括号要加上去,否则下次调用token = st.nextToken(")")得到的是上次剩下来的),而不是向后扩展的)
//就算)其实是不做任何处理的,但不能忽视调这个,否则调用nextToken得到的可能是)而不是运算数
if (in.equals(")")) {
//先来一个左括号,再来一个右括号就完了,说明是一对空括号
throw new RuntimeException("请勿输入空括号!");
}
in += st.nextToken();
int index = -1;
/*
如同(a+b+(c+d)+f),外层括号( 会与 内层括号) 匹配得到 a+b+(c+d 而不是我们认为的外层括号的所有内容
可以通过检测目前得到的内部内容in,分析in内是否再此出现( 推断出是否外层括号与内层括号相匹配
而且可能内层不止嵌套一对括号,可能多对,这就需要分析有多少对( 去推断相应有多少)
但又不能单纯地计算目前得到的内部内容in有多少个(,因为可能在找)时扩展又加入了(
如:((a+b)+(c+d)+f),依靠获取(括号的个数的方法会得到((a+b)+(c+d),此时是左3右2(最外层的用于判断没加入in,也没必要)
为解决此问题引入一个指针index记录(的位置,下次从index后寻找是否还有(
*/
while ((index = in.indexOf("(", index + 1)) != -1) {
//比如(a+b)
//相当于token = st.nextToken(")"),为填充括号内的东西,这里就是a+b
//may可能是内容,也可能是)
String may = st.nextToken();
if (may.equals(")")) {
//若是),就说明前面就把填充括号内的内容的活干完了,就只走一步:补右括号
in += may;
} else {
//填充内容
in += may;
//为了补上),这里就是a+b)
in += st.nextToken();
}
}
int resIn = compute(in);
numStack.push(resIn);
}
//右括号就直接忽略不做任何操作
}
//运行到这,肯定所有token入栈,且最多3个操作数,2个操作符
while (!opeStack.isEmpty()) {
num1 = numStack.pop();
num2 = numStack.pop();
operate = opeStack.pop();
result = cal(num1, num2, operate);
numStack.push(result);
}
result = numStack.pop();
/*
//()感觉还是不太合理,还是直接抛出去吧
//特殊情况,个人感觉只有()这种空括号才会导致numStack空
if (!numStack.isEmpty()) {
result = numStack.pop();
} else {
//这里其实可以考虑抛出异常,但还是借鉴win10计算机空括号为0的设定
result = 0;
}*/
return result;
}
/**
* 输入两个运算数,一个运算符,得到运算结果
* 由于栈先入后出的特点,被减数和被除数会后被取出,即num2
*
* @param num1 运算数1
* @param num2 运算数2
* @param ope 运算符
* @return 运算结果
*/
static int cal(int num1, int num2, int ope) {
int res = 0;
switch (ope) {
case '+':
res = num2 + num1;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num2 * num1;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
static boolean isOperate(String s) {
return s.matches("[+\\-*/]");
}
static boolean isDigit(String s) {
return s.matches("-?\\d+");
}
static int priority(int ope) {
//数字越大,优先级越高
if (ope == '+' || ope == '-') {
return 0;
} else if (ope == '*' || ope == '/') {
return 1;
} else {
return -1;
}
}
}
另外一些注意点:
StringTokenizer的用法
StringTokenizer st = new StringTokenizer(exp, regex, true);
构造器可以用三个参数的,这样设置returnDelims为ture,可以分隔符也会当做token被返回
这是我目前唯一知道的会返回分割符的分割字符串手段
String in = st.nextToken(")");
带参数的nextToken(regex),可以重新设置分割符,并且改变的不仅仅是这一次,后面调用不带参数的nextToken(),此时的分隔符是重新设置过的regex
假设 st 的 exp 是"abc",则调用in = st.nextToken("c"),in = "ab",即不会包含分割符c,分割符会在下次调用nextToken时被获取到(前提是returnDelims为ture,默认为false)
使用正则可以区分开负数符号- 和 运算符-,但StringTokenizer任会把-与数字分开,原因不详,负数开头用StringTokenizer只能单独考虑。
@Test
public void test4() {
String regex = "\\b[+\\-*/]";
String exp = "-23*3+1-24-(-8-2+(-5))";
String s = exp.replaceAll(regex, "#");
System.out.println("s = " + s);
}
![](https://i-blog.csdnimg.cn/blog_migrate/bc52d666ec5581365398692cbcdc023b.png)
测试输出
本人也是小白,若有错误欢迎指出讨论😘😘😘