计算器
用栈实现计算器是一种非常常见的算法题,我们先来看下面这道题:
简单来讲就是给你一个算式字符串,通过算法输出运算结果,这里没有涉及到带括号的问题,就是简单加减乘除运算。首先你要搞清楚给我们的是字符串,我们要是想加减运算就要先给他转成数字。这里实现起来很简单,遍历数组,只要没有遍历到符号就要将数字提取出来,一位数还好是他本身,如果是两位或者三位数就要采取一些运算,这里我是这样写的num = num * 10 + c - '0'
。这里的c是遍历字符串时单个字符,num初始值是0,表示转换后的数字。有人会疑问为啥要c-'0'
?我们要注意c是字符不是int数字,字符 '0'的编码值为 48, 而字符 '1' 到 '9' 的编码值为 49 到 57。因此,将字符数字减去字符 '0'就可以将其转换为对应的数字值。
好,搞定了数字的处理,下面就要考虑运算顺序了,当+和*同时存在时如何保证先算*。由于乘除优先于加减计算,因此不妨考虑先进行所有乘除运算,并将这些乘除运算后的整数值放回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。
基于此,我们可以用一个栈,保存这些(进行乘除运算后的)整数的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。
具体来说,遍历字符串,并用变量preSign记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据preSign来决定计算方式:
加号:将数字压入栈;减号:将数字的相反数压入栈;乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。代码实现中,若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新preSign为当前遍历的字符。
代码如下:
public int calculate(String s) {
Deque<Integer> stack = new ArrayDeque ();
int len=s.length();
char proSign='+';
int num=0;
for(int i=0;i<len;i++){
char c = s.charAt(i);
if(Character.isDigit(c)){
num = num * 10 + c - '0' ;
}
if(!Character.isDigit(c)&&c!=' '|| i==len-1){
if(proSign=='*'){
stack.push(stack.pop() * num);
}else if(proSign=='/'){
stack.push(stack.pop() / num);
}else if(proSign=='-'){
stack.push(-num);
}else {
stack.push(num);
}
proSign=c;
num=0;
}
}
int ans=0;
while(!stack.isEmpty()){
ans+=stack.pop();
}
return ans;
}
当然这种情况下我们只能处理简单的加减乘除,如果有括号或者较为复杂的括号嵌套,这种方法可就不行了。关于这类情况就不得不提到我们下面要说的波兰表达式了。
波兰表达式
什么是波兰表达式?
我们先理解一下什么是表达式,表达式就是小学里学的类似(2+1)*3)这样的式子,根据不同的记法,有前缀、中缀和后缀三种方式,其区别在于运算符相对于操作数的位置,前缀表达式的运算符位于操作数之前,中缀和后缀同理,如下图,其实这就对应了树的前中后三种遍历方式。如图:
对应了三种表达式:
- 中缀表达式:1+(2+3)×4-5
- 前缀表达式:-+1×+2345
- 后缀表达式:123+4×+5-
我们可一看到好像就中缀表达式我们能看懂,其他都是啥啊?当然,前缀和后缀是给计算机看的,如果我们直接将中缀式给计算机他处理起来会很麻烦,我们就可以将中缀式转换层前缀或者后缀式,这样就方便多了(具体我们待会讲)。我们通常将前缀式叫做波兰式,后缀时叫做逆波兰式,这就是波兰式的由来。
下面是转换方式,感兴趣的可以看一下
- 中缀式转后缀式
- 从左到右扫描中缀式,遇到数值时直接输出。
- 遇到运算符时,若其优先级高于栈顶运算符,则将其压入栈中;否则,将栈顶运算符弹出并输出,重复此操作,直到运算符的优先级低于栈顶运算符为止。最后将运算符入栈。
- 当扫描到中缀式的末尾时,将栈中所有运算符弹出并输出。
例如,中缀式表达式 2 + 3 * 4 - 5,其对应的后缀式为 2 3 4 * + 5 -。
- 中缀式转前缀式
- 与中缀式转后缀式类似,从右至左扫描中缀式。
- 遇到数值时直接输出。
- 遇到运算符时,若其优先级低于栈顶运算符,则将其压入栈中;否则,将栈顶运算符弹出并输出,重复此操作,直到运算符的优先级高于栈顶运算符为止。最后将运算符入栈。
- 当扫描完成时,将栈中所有运算符依次弹出并输出即为前缀式。
例如,中缀式表达式 2 + 3 * 4 - 5,其对应的前缀式为 - + 2 * 3 4 5。
- 后缀式转中缀式
要将后缀式转换为中缀式,可以从左到右扫描后缀式,遇到数值时将其压入栈中,遇到运算符时将栈顶的两个数值弹出并计算,并将运算符插入两个数值之间,最后输出栈中的数值即可。例如,后缀式表达式 2 3 4 * + 5 - 对应的中缀式为 2 + 3 * 4 - 5。
- 后缀式转前缀式
可以通过将后缀式从右往左扫描,并将数值顺序压入栈中,遇到运算符时弹出两个数值,构造以运算符为根节点的表达式树,最后前序遍历表达式树即可得到前缀式。例如,后缀式表达式2 3 4 * + 5 -对应的前缀式为- + * 3 4 2 5。
那下面我们来看一道题吧:
给你一个逆波兰表达式(后缀式)式子的结果
仔细观察后缀表达式可以发现,其特点就是数字先保存下来,然后遇到符号就计算,例如”123+”,遇至+号就将2+3加起来变成5再继续其他操作,直到最后完成。
如果用栈来解释就是遇见数字即进栈,遇见运算符,则取出栈中最上面的两个元素进行计算,最后将运算结果入栈。实现代码其实很容易:
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack();
for(String token:tokens){
//如果是数字则入栈
if(!Character.isDigit(token.charAt(0))&&token.length()==1){
int b = stack.pop();
int a = stack.pop();
switch(token){
case "+": stack.push(a+b);break;
case "-": stack.push(a-b);break;
case "*": stack.push(a*b);break;
case "/": stack.push(a/b);break;
}
}else{
stack.push(Integer.parseInt(token));
}
}
return stack.peek();
}