上一篇博客我們講了隊列的擴展應用,本篇我們再來分析一下棧的一些應用。
計算器
說到棧的應用,最經典的就是計算器的實現了。對於給定的一個算數表達式,我們需要輸出它的最終結果。
例如:對於輸入(10+2-5)*(30/6) 我們需要輸出35
分析過程
- 對於一個計算表達式,我們需要考慮運算符的優先級,還要考慮括號。按照我們人類的計算思路,會把要先運算的算好再與其他繼續運算。
例如:
1+2*(4-3)
=>1+2*1
=>1+2
=>3
- 一個表達式中有兩種數據:數字,運算符號(括號也算運算符號)。我們需要考慮的是使用一個棧還是使用兩個棧?如果使用一個棧,當往棧裏面放數字我們需要做什麼處理?放運算符號呢?
- 我們仔細觀察表達式的話,會發現一個運算符加上左右兩側的數字能夠構成一個獨立的表達式(不考慮括號的場景);如果有括號的話,左括號和右括號中間也是一個獨立的表達式;
- 表達式經過運算就是一個數字;
- 使用棧計算表達式必然需要兩個過程:入棧,出棧。對於出棧,我們一定希望先取出來的是優先級比較高的表達式,後取出來的是優先級比較低的表達式。所以就需要我們在表達式入棧的時候進行一波整理。把所有的運算邏輯按運算優先級調整入棧順序。加法和減法相對於乘法除法必定位於棧底部,括號運算由於過於複雜,在入棧的時候我們就把括號裏面的表達式變成一個數字
雙棧實現
/**
* 用兩個棧實現的計算器
*/
public class StackCalculator {
/**
* 表達式
*/
private String expr;
/**
* 操作數棧
*/
private Stack<Number> numStack;
/**
* 操作符棧
*/
private Stack<Character> operStack;
public StackCalculator(String expr) {
//去除表達式中的空格
this.expr = expr.replace(" ", "");
numStack = new Stack<>();
operStack = new Stack<>();
}
/**
* 計算表達式的值
*/
public Number calculate() {
char[] chars = expr.toCharArray();
//入棧
for (int i = 0; i < chars.length; i++) {
//如果是數字則直接入棧
if (Character.isDigit(chars[i])) {
String numString = String.valueOf(chars[i]);
//此處考慮多位數和小數的情況 如果下一位還是數字或者小數點則繼續向下取值
while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
i++;
numString += chars[i];
}
numStack.push(new BigDecimal(numString));
continue;
}
//如果是運算符號 我們就需要在入棧的時候檢查優先級順序
//1、如果operStack裏面沒有數據則直接入棧
//2、如果operStack裏面有數據,則需要比較棧頂運算符與本次運算符的優先級
//2.1、如果本次優先級比較大,則放入棧
//2.2、如果棧裏面的優先級比較大,則從operStack取出棧頂運算符,從numStack取出兩個數字進行運算,再把運算結果放回numStack
//3、如果本次運算符是左括號,則放入棧
//4、如果本次運算符是右括號,則不停地從operStack彈出運算符直到彈出左括號,把這兩個括號中間的表達式算出結果,放入numStack
if (operStack.size() == 0) {
operStack.push(chars[i]);
continue;
}
if (chars[i] == '(') {
operStack.push(chars[i]);
continue;
}
if (chars[i] == ')') {
while (operStack.top() != '(') {
calResult();
}
operStack.pop();
continue;
}
if (compareOper(chars[i], operStack.top())) {
operStack.push(chars[i]);
} else {
calResult();
operStack.push(chars[i]);
}
}
//出棧得出最後計算結果
while (operStack.size() != 0) {
calResult();
}
return numStack.pop();
}
/**
* 這個方法比較重要,巧妙的設計能夠簡便我們的運算
* 當oper1和oper2的優先級一樣的時候返回false,這樣子就能保證在不考慮括號的情況下 operStack裏面的數據量不超過2個
* <p>
* 1、stackOper是乘除法 返回false
* 2、putOper和stackOper同爲加減法 返回false
* 3、stackOper是左括號 返回true
* 4、其他情況(putOper是乘除法 stackChar是加減法) 返回true
*/
private boolean compareOper(char putOper, char stackOper) {
if (stackOper == '*' || stackOper == '/') {
return false;
}
if ((putOper == '+' || putOper == '-') && (stackOper == '+' || stackOper == '-')) {
return false;
}
if (stackOper == '(') {
return true;
}
return true;
}
/**
* 計算一次值 operStack取出一個值 numStack取出兩個值 算好以後放進numStack
*/
private void calResult() {
Character oper = operStack.pop();
//要注意第二次取出來的是 第一個操作數
Number num2 = numStack.pop();
Number num1 = numStack.pop();
BigDecimal result = null;
if (oper == '+') {
result = new BigDecimal(String.valueOf(num1)).add(new BigDecimal(String.valueOf(num2)));
} else if (oper == '-') {
result = new BigDecimal(String.valueOf(num1)).subtract(new BigDecimal(String.valueOf(num2)));
} else if (oper == '*') {
result = new BigDecimal(String.valueOf(num1)).multiply(new BigDecimal(String.valueOf(num2)));
} else if (oper == '/') {
result = new BigDecimal(String.valueOf(num1)).divide(new BigDecimal(String.valueOf(num2)), BigDecimal.ROUND_HALF_UP);
}
numStack.push(result);
}
public static void main(String[] args) {
StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)");
System.out.println(stackCalculator.calculate());
}
}
這裏在原先的Stack中增加了一個top()方法
/**
* 查看棧頂的值 不取出
*/
public T top() {
//如果棧空了則返回null
if (isEmpty()) {
System.out.println("棧空了");
return null;
}
return first.item;
}
具體的實現細節,都很詳細的寫在了註釋中。上述代碼是使用了雙棧,分別存放操作數和操作符。如果使用一個棧,當然也可以。每次計算的時候固定從stack中取三個元素。
前綴,中綴,後綴表達式
這幾種表達式的不同之處僅僅在於操作數與操作符的相對位置不同。前中後是針對操作符而言的。以一個簡單的數學表達式1+2*3舉例
前綴表達式:+ 1 * 2 3
中綴表達式:1 + 2 * 3
後綴表達式:1 2 3 * +
表達式之間的相互轉換思路:
1、如果大家能夠很好的理解上面計算器的實現方式,對於表達式的轉換應該也很容易就能想到;
2、利用棧FIFO的特性,一開始在表達式入棧的時候按照一定規律整理好。把不按照規律的表達式,在入棧時直接計算出一個臨時結果放入棧中,最後按次序出棧得到最後的結果。
中綴轉前綴
/**
* 在原來的計算器類中新加入操作符棧
*/
private Stack<Character> operStack;
/**
* 中綴表達式轉前綴表達式
*/
public String infix2Prefix() {
char[] chars = expr.toCharArray();
//入棧
for (int i = 0; i < chars.length; i++) {
//如果是數字則直接入棧
if (Character.isDigit(chars[i])) {
String numString = String.valueOf(chars[i]);
//此處考慮多位數和小數的情況 如果下一位還是數字或者小數點則繼續向下取值
while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
i++;
numString += chars[i];
}
exprStack.push(numString);
continue;
}
//如果是運算符號 我們就需要在入棧的時候檢查優先級順序
//1、如果operStack裏面沒有數據則直接入棧
//2、如果operStack裏面有數據,則需要比較棧頂運算符與本次運算符的優先級
//2.1、如果本次優先級比較大,則放入棧
//2.2、如果棧裏面的優先級比較大,則從operStack取出棧頂運算符,從exprStack取出兩個表達式進行拼接,再把運算結果放回exprStack
//3、如果本次運算符是左括號,則放入棧
//4、如果本次運算符是右括號,則不停地從operStack彈出運算符直到彈出左括號,把這兩個括號中間的表達式算出結果,放入exprStack
if (operStack.size() == 0) {
operStack.push(chars[i]);
continue;
}
if (chars[i] == '(') {
operStack.push(chars[i]);
continue;
}
if (chars[i] == ')') {
while (operStack.top() != '(') {
getPrefix();
}
operStack.pop();
continue;
}
if (compareOper(chars[i], operStack.top())) {
operStack.push(chars[i]);
} else {
getPrefix();
operStack.push(chars[i]);
}
}
//出棧得出最後計算結果
while (operStack.size() != 0) {
getPrefix();
}
return exprStack.pop();
}
private void getPrefix() {
String expr2 = exprStack.pop();
String expr1 = exprStack.pop();
Character oper = operStack.pop();
//前綴表達式 符號在前
String tempResult = oper + " " + expr1 + " " + expr2;
exprStack.push(tempResult);
}
public static void main(String[] args) {
StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)");
// System.out.println(stackCalculator.calculate());
System.out.println(stackCalculator.infix2Prefix());
}
我們可以看到其實總的流程跟計算器是一模一樣的,修改的點只是把原來的calResult方法改爲getPrefix。相信大家應該也很容易理解。
中綴轉後綴
中綴轉後綴的思路與中綴轉前綴的思路一樣,只是需要把getPrefix的實現改爲getSuffix。
private void getSuffix() {
String expr2 = exprStack.pop();
String expr1 = exprStack.pop();
Character oper = operStack.pop();
//後綴表達式 符號在前
String tempResult = expr1 + " " + expr2 + " " + oper;
exprStack.push(tempResult);
}
前綴轉後綴
這個留給大家自己實現吧~
其實百分之95的代碼跟中綴轉後綴一樣。只不過需要在過程加一點判斷,防止一些運行時異常發生,如數組越界異常。然後對於小數和多位數的處理也略有不同。
總結
爲什麼我們在實現計算器的時候要使用棧,而不是隊列?
對於3個值A,B,C。設入棧(隊列)的順序爲ABC,現在A已經放入了棧(隊列),正要把B也按照一定邏輯放入棧(隊列)中,現在需要特定來源的數據來給我們的邏輯提供支撐。
如果使用隊列,對於B的處理邏輯,我們需要依靠C,也就是說我們需要依靠未來的值來處理現在的邏輯
如果使用棧,對於B的處理邏輯,我們需要依靠A,也就是說我們可以依靠過去的值來處理現在的邏輯
這是我理解中棧與隊列的選型最大的差別。