题干: https://leetcode.com/problems/basic-calculator-ii/
要实现一个简单的加减乘除计算器,假设输入都是合法的计算字符串,不带括号,只有数字,空格,和加减乘除四则运算符号。
这题的思路其实本质上就是实现一个语法解析器,让程序从一个字符串知道你想要做什么运算即可。
大家想想语法解析器(例如编译器)通常是咋做的,就是从左到右扫描,每扫描一步,更新某个状态,直到扫描完毕,状态获得最终值,获得完整语义。
这里大体也是这样,从左到右扫描字符串,至于这个过程要更新什么状态,则就是需要分析的问题。我的方法论是纸上画图,除非你的空间想象能力爆表。
结合四则运算先乘除后加减的法则,
画图的过程中我自己总结了这么一个方法可以解决上面的语法解析的问题:
保存一个临时串,用来收集数字,开始为空串,碰到数字就把数字追加到其中,直到碰到一个四则运算符,说明这个数字已经收集完毕,将这个数字保存起来,我这里设计了一个栈结构,该栈用于做数字的临时保存。
例如123+2*4
从左到右扫描1,2,3,逐步收集数字为1, 12, 123直到扫描到+号时,说明该数字收集完毕,则把123入栈。并重置临时字符串为空串
加号本身我用也用一个栈保存起来,画图的过程中发现用双向队列实现比较好,因为最后运算的时候需要从栈底反着遍历。
接下来继续往下扫描碰到2,则老规矩,继续把2追加到临时字符串
扫描到下一个乘法运算符后,将2也入栈,并将*入运算符栈
下一步扫描到4,已经到了末尾,则将该数字入栈。
允许我卖个关子,上面的步骤并非我的最终实现步骤,因为大家会发现这里面的问题,就是加减乘除都混在一个栈里,最后这个计算顺序很难控制。
于是实际上我设计了两个栈用来保存运算数字,分别记为A和B吧,其中B栈只用于计算乘除法,而A栈只用于计算加减法。
具体如何操作呢?因为乘除法要先于加减法,所以在扫描的过程中,我会尽量把碰到的乘除法运算法借助B栈优先算出来,然后把算出的结果push进A栈,这样,最终A栈就可以直接只算加减法了,并且在计算完乘除法后,操作符栈(记为C吧)中也只剩下加减符号,而乘除符号在运算过程中被pop出去了。
总结一下就是:一切数字都先入B栈,在入栈时,如果发现操作符栈的顶端是*或者/符号,则将B栈顶端的两个数字立即进行相应的运算,运算结果替换掉顶端两个数字,并将操作符栈顶运算符pop出去,如果碰到2个以上数字连乘,可以想象这招也是奏效的。直到碰到下一个加减符号,说明本段乘除运算完毕,就可以把乘除运算的中间结果推至A栈了。
边界条件:只有乘除运算,没有加减运算
最后就是三个栈,示意图如下:
下图画了一个案例的最终栈状态。
然后从底部往上收集,并做简单推算即可。也就是1-24+9
代码如下:
package com.example.demo.leetcode;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;
public class BasicCalculatorII {
private int isNumber(char c){
if(c>='0' && c<='9'){
// digit
return 1;
}else if(c==' '){
// space
return 0;
}else{
// operator
return -1;
}
}
private void calcMultiOrDivOnStacks(Deque<Character> dq4operator, Stack<Integer> stack4muldiv){
if(!dq4operator.isEmpty() && (dq4operator.getFirst()=='*' || dq4operator.getFirst()=='/')){
int op2 = stack4muldiv.pop();
int op1 = stack4muldiv.pop();
if(dq4operator.getFirst()=='*'){
int newopval = op1*op2;
stack4muldiv.push(newopval);
}else{
int newopval = op1/op2;
stack4muldiv.push(newopval);
}
//乘除操作符弹出
dq4operator.removeFirst();
}
}
public int calculate(String s) {
Deque<Integer> dq4sumsubtract = new ArrayDeque<>();
Deque<Character> dq4operator = new ArrayDeque<>();
Stack<Integer> stack4muldiv = new Stack<>();
// 从左到右扫描字符串,并做一些事情
String opNum="";
for(int i=0;i<s.length();i++){
if(isNumber(s.charAt(i))==1){
// 如果是数字,追加入opNum
opNum=opNum+s.charAt(i);
}
if(i==s.length()-1){
//最后一步特殊处理,数字先入stack4muldiv, opNum置空
if(opNum.length()>0){
stack4muldiv.push(Integer.valueOf(opNum));
opNum = "";
calcMultiOrDivOnStacks(dq4operator, stack4muldiv);
dq4sumsubtract.addFirst(stack4muldiv.pop());
}
continue;
}
if(isNumber(s.charAt(i))==0){
// 空格忽略
continue;
}
if(isNumber(s.charAt(i))==-1){
// 扫到操作符
// 数字先入栈,并重置opNum为空
stack4muldiv.push(Integer.valueOf(opNum));
opNum = "";
// 如果此时操作符栈顶为*或者/,则弹出之,并对stack4muldiv栈顶两个数字做相应的运算
calcMultiOrDivOnStacks(dq4operator, stack4muldiv);
// 运算符
if(s.charAt(i)=='+' || s.charAt(i)=='-'){
// 如果是加减运算符,则出栈直接推到纯加减的双向队列
dq4sumsubtract.addFirst(stack4muldiv.pop());
dq4operator.addFirst(s.charAt(i));
}
if(s.charAt(i)=='*' || s.charAt(i)=='/'){
dq4operator.addFirst(s.charAt(i));
}
}
}
return doWith2Stacks(dq4sumsubtract, dq4operator);
}
private int doWith2Stacks(Deque<Integer> dq4sumsubtract, Deque<Character> dq4operator){
int ret = 0;
Integer op1=null;
Integer op2=null;
while(!dq4operator.isEmpty()){
Character op = dq4operator.removeLast();
if(op1==null){
op1 = dq4sumsubtract.removeLast();
}
op2 = dq4sumsubtract.removeLast();
ret = basicCalc(op1, op2, op);
op1 = ret;
}
if(!dq4sumsubtract.isEmpty()){
return dq4sumsubtract.getLast();
}
return ret;
}
int basicCalc(int op1, int op2, Character op){
if(op=='+'){
return op1+op2;
}else if(op=='-'){
return op1-op2;
}else{
throw new RuntimeException("illegal input in basicCalc");
}
}
public static void main(String[] args) {
// String input = "1+2*3+8";
// String input = " 1 + 2 * 3";
// String input = " 1 - 2*3 + 9";
String input = " 3/2 ";
BasicCalculatorII demo = new BasicCalculatorII();
int ret = demo.calculate(input);
System.out.println(ret);
}
}
反思:这题我依然偷懒而高估了自己的空间想象力,一开始没有手画,浪费了不少时间。后来手画之后,发现这问题并不复杂。
另外就是依然没有习惯性地去做边界条件思考,例如只有1个数字的情况