題幹: 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個數字的情況