通過資料結構與演算法——棧(四)逆波蘭計算器-字尾表示式的程式碼實現,可以看到:字尾表示式對於計算機來說很方便,但是對於我們人來說,字尾表示式卻不是那麼容易寫出來的。
所以本篇就是來講解怎麼實現中綴表示式轉換成字尾表示式,以及完成完整版的逆波蘭計算器。
* 中綴表示式轉字尾表示式步驟
-
初始化兩個棧:
- 運算子棧:s1
- 中間結果棧:s2
-
從左到右掃描中綴表示式
-
遇到運算元時,將其壓入 s2
-
遇到運算子時
比較 它 與 s1 棧頂運算子的優先順序:
(1)如果 s1 為空,或則棧頂運算子號為
(
,則將其壓入符號棧 s1 (2)否則,若優先順序比棧頂運算子 高,也將其壓入符號棧 s1
(3)否則,若優先順序比棧頂運算子 低 或 相等,將 s1 棧頂的運算子 彈出,並壓入到 s2 中
再重複第 4.1 步驟,與新的棧頂運算子比較(因為 4.3 將 s1 棧頂運算子彈出了)
這裡重複的步驟在實現的時候有點難以理解,下面進行解說:
如果 s1 棧頂符號 優先順序比 當前符號 高或則等於,那麼就將其 彈出,壓入 s2 中(迴圈做,是隻要 s1 不為空),如果棧頂符號為
(
,這裡不把(
當作運算子,所以碰到了就不用作比較了,(
也不用彈出,直接 把當前運算子壓入即可。但如果當前運算子為左括號(
或者 右括號)
呢?那就看下面的 第5點 -
遇到括號時:
(1)如果是左括號
(
:則直接壓入 s1 (2)如果是右括號
)
: 則依次彈出 s1 棧頂的運算子,並壓入 s2,直到遇到 左括號(
為止,此時將這一對括號【 即當前的右括號)
和碰到的第一個 棧頂的左括號(
】丟棄。 -
重複步驟 2 到 5,直到表示式最右端
-
後將 s1 中的運算子依次彈出並壓入 s2
-
依次彈出 s2 中的元素並輸出,結果的 逆序 即為:中綴表示式轉字尾表示式
下面進行舉例說明:
將中綴表示式:1+((2+3)*4)-5
轉換為字尾表示式
掃描到的元素 | s2 (棧底 -> 棧頂) | s1(棧底 -> 棧頂) | 說明 |
---|---|---|---|
1 | 1 |
空 | 遇到運算元,將其壓入 s2 |
+ |
1 |
+ |
s1 棧為空,將其壓入 s1 |
( |
1 |
+ ( |
是左括號,直接壓入 s1 |
( |
1 |
+ ( ( |
是左括號,直接壓入 s1 |
2 | 1 2 |
+ ( ( |
遇到運算元,將其壓入 s2 |
+ |
1 2 |
+ ( ( + |
遇到操作符:與 s1 棧頂運算子比較,棧頂為 ( ,直接將其壓入 s1 |
3 | 1 2 3 |
+ ( ( + |
遇到運算元,將其壓入 s2 |
) |
1 2 3 + |
+ ( |
遇到右括號:彈出運算子直至遇到左括號,這裡彈出 s1 中的 + 壓入 s2 中,這裡去掉這一對小括號 |
* |
1 2 3 + |
+ ( * |
遇到操作符:與 s1 棧頂比較,棧頂為 ( ,直接將其壓入 s1 棧 |
4 | 1 2 3 + 4 |
+ ( * |
遇到運算元:將其壓入 s2 |
) |
1 2 3 + 4 * |
+ |
遇到右括號:彈出運算子直至遇到左括號,這裡彈出 s1 中的 * 壓入 s2 中,這裡去掉這一對小括號 |
- |
1 2 3 + 4 * + |
- |
遇到操作符:與 s1 棧頂比較,優先順序一致,將 s1 中的 + 彈出,並壓入 s2 中,再將- 壓入s1 |
5 | 1 2 3 + 4 * + 5 |
- |
遇到運算元:將其壓入 s2 |
到達最右端 | 1 2 3 + 4 * + 5 - |
空 | 解析完畢,將 s1 中的符號彈出並壓入 s2 中 |
由於 s2 是一個棧,彈出是從棧頂彈出,因此逆序後結果就是 1 2 3 + 4 * + 5 -
我的疑問
你怎麼知道這個中綴表示式轉字尾表示式的思路是這樣的?
在學習和使用上有兩個層次:
- 應用層次:別人發明出來的東西,你學習、理解它,並靈活運用它
- 自創:你自己發明一個東西出來,並使用它
那麼這裡的中綴轉字尾表示式的思路步驟,則屬於第一個層次,相關的計算機專家之類的,發明出來了。我們要理解它並靈活運用它。等你能力達到一定層度時,有可能發明出來一個演算法。
再比如:絕世武功 -> 降龍十八掌,別人已經創造出來了,你不去學習理解它,如何加以改進並自創?如果沒有人教你,你怎麼能學會降龍十八掌?
程式碼實現
/**
* 中綴表示式轉字尾表示式
*/
public class InfixToSuffix {
public static void main(String[] args) {
InfixToSuffix infixToSuffix = new InfixToSuffix();
// 目標:1+((2+3)*4)-5 轉為 1 2 3 + 4 * + 5 -
// 1. 將中綴表示式轉成 List,方便在後續操作中獲取資料
String infixExpression = "1+((2+3)*4)-5";
List<String> infixList = infixToSuffix.toInfixExpressionList(infixExpression);
System.out.println(infixList); // [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
// 2. 將中綴表示式轉成字尾表示式
List<String> suffixList = infixToSuffix.parseSuffixExpreesionList(infixList);
System.out.println(suffixList); // [1, 2, 3, +, 4, *, +, 5, -]
}
/**
* 將中綴表示式解析成單個元素的 List,
*
* @param s
* @return 1+((2+3)*4)-5 -> [1,+,(,(,2,+,3,),*,4,),5]
*/
//方法:將 中綴表示式轉成對應的List
// s="1+((2+3)×4)-5";
public static List<String> toInfixExpressionList(String s) {
//定義一個List,存放中綴表示式 對應的內容
List<String> ls = new ArrayList<String>();
int i = 0; //這時是一個指標,用於遍歷 中綴表示式字串
String str; // 對多位數的拼接
char c; // 每遍歷到一個字元,就放入到c
do {
//如果c是一個非數字,我需要加入到ls
if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
ls.add("" + c);
i++; //i需要後移
} else { //如果是一個數,需要考慮多位數
str = ""; //先將str 置成"" '0'[48]->'9'[57]
while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
str += c;//拼接
i++;
}
ls.add(str);
}
} while (i < s.length());
return ls;//返回
}
/**
* 中綴表示式 List 轉為字尾表示式 List
*
* @param ls
* @return
*/
//即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
//方法:將得到的中綴表示式對應的List => 字尾表示式對應的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
//定義兩個棧
Stack<String> s1 = new Stack<String>(); // 符號棧
//說明:因為s2 這個棧,在整個轉換過程中,沒有pop操作,而且後面我們還需要逆序輸出
//因此比較麻煩,這裡我們就不用 Stack<String> 直接使用 List<String> s2
//Stack<String> s2 = new Stack<String>(); // 儲存中間結果的棧s2
List<String> s2 = new ArrayList<String>(); // 儲存中間結果的Lists2
//遍歷ls
for (String item : ls) {
//如果是一個數,加入s2
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
//如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//!!! 將 ( 彈出 s1棧, 消除小括號
} else {
//當item的優先順序小於等於s1棧頂運算子, 將s1棧頂的運算子彈出並加入到s2中,再次轉到(4.1)與s1中新的棧頂運算子相比較
//問題:我們缺少一個比較優先順序高低的方法。 下面建立了priority類,用來比較優先順序
//這裡比較許可權的時候,可能在比較過程中取到括號,但是問題不大,因為方法規定了,不符合運算子的預設優先順序為0
while (s1.size() != 0 && priority.getValue(s1.peek()) >= priority.getValue(item)) {
s2.add(s1.pop());
}
//還需要將item壓入棧
s1.push(item);
}
}
//將s1中剩餘的運算子依次彈出並加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2; //注意因為是存放到List, 因此按順序輸出就是對應的字尾表示式對應的List
}
}
/**
* 計算操作符號優先順序,暫時只支援 + - * /
*
* @return 優先順序越高,數值越大
*/
//編寫一個類 priority 可以返回一個運算子 對應的優先順序
class priority {
private static int ADD = 1;//加
private static int SUB = 1;//減
private static int MUL = 2;//乘
private static int DIV = 2;//除
//寫一個靜態方法,返回對應的優先順序數字
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在該運算子" + operation);
break;
}
return result;
}
}
測試輸出
[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
不存在該運算子(
不存在該運算子(
[1, 2, 3, +, 4, *, +, 5, -]
可以看到,已經變成字尾表示式的順序了。下面結合前面實現的逆波蘭計算器整合中綴表示式轉字尾表示式。
完整版逆波蘭計算器
public class PolandNotation {
public static void main(String[] args) {
//完成將一箇中綴表示式轉成字尾表示式的功能
//說明
//1. 1+((2+3)×4)-5 => 轉成 1 2 3 + 4 × + 5 –
//2. 因為直接對str 進行操作,不方便,因此 先將 "1+((2+3)×4)-5" =》 中綴的表示式對應的List
// 即 "1+((2+3)×4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
//3. 將得到的中綴表示式對應的List => 字尾表示式對應的List
// 即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
String expression = "1+((2+3)*4)-5";//注意表示式
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中綴表示式對應的List=" + infixExpressionList); // ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
System.out.println("字尾表示式對應的List" + suffixExpreesionList); //ArrayList [1,2,3,+,4,*,+,5,–]
System.out.printf("expression=%d", calculate(suffixExpreesionList)); // ?
}
//即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
//方法:將得到的中綴表示式對應的List => 字尾表示式對應的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
//定義兩個棧
Stack<String> s1 = new Stack<String>(); // 符號棧
//說明:因為s2 這個棧,在整個轉換過程中,沒有pop操作,而且後面我們還需要逆序輸出
//因此比較麻煩,這裡我們就不用 Stack<String> 直接使用 List<String> s2
//Stack<String> s2 = new Stack<String>(); // 儲存中間結果的棧s2
List<String> s2 = new ArrayList<String>(); // 儲存中間結果的Lists2
//遍歷ls
for (String item : ls) {
//如果是一個數,加入s2
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
//如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//!!! 將 ( 彈出 s1棧, 消除小括號
} else {
//當item的優先順序小於等於s1棧頂運算子, 將s1棧頂的運算子彈出並加入到s2中,再次轉到(4.1)與s1中新的棧頂運算子相比較
//問題:我們缺少一個比較優先順序高低的方法
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
//還需要將item壓入棧
s1.push(item);
}
}
//將s1中剩餘的運算子依次彈出並加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2; //注意因為是存放到List, 因此按順序輸出就是對應的字尾表示式對應的List
}
//方法:將 中綴表示式轉成對應的List
// s="1+((2+3)×4)-5";
public static List<String> toInfixExpressionList(String s) {
//定義一個List,存放中綴表示式 對應的內容
List<String> ls = new ArrayList<String>();
int i = 0; //這時是一個指標,用於遍歷 中綴表示式字串
String str; // 對多位數的拼接
char c; // 每遍歷到一個字元,就放入到c
do {
//如果c是一個非數字,我需要加入到ls
if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
ls.add("" + c);
i++; //i需要後移
} else { //如果是一個數,需要考慮多位數
str = ""; //先將str 置成"" '0'[48]->'9'[57]
while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
str += c;//拼接
i++;
}
ls.add(str);
}
} while (i < s.length());
return ls;//返回
}
//完成對逆波蘭表示式的運算
/*
* 1)從左至右掃描,將3和4壓入堆疊;
2)遇到+運算子,因此彈出4和3(4為棧頂元素,3為次頂元素),計算出3+4的值,得7,再將7入棧;
3)將5入棧;
4)接下來是×運算子,因此彈出5和7,計算出7×5=35,將35入棧;
5)將6入棧;
6)最後是-運算子,計算出35-6的值,即29,由此得出最終結果
*/
public static int calculate(List<String> ls) {
// 建立給棧, 只需要一個棧即可
Stack<String> stack = new Stack<String>();
// 遍歷 ls
for (String item : ls) {
// 這裡使用正規表示式來取出數
if (item.matches("\\d+")) { // 匹配的是多位數
// 入棧
stack.push(item);
} else {
// pop出兩個數,並運算, 再入棧
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("運算子有誤");
}
//把res 入棧
stack.push("" + res);
}
}
//最後留在stack中的資料是運算結果
return Integer.parseInt(stack.pop());
}
}
//編寫一個類 Operation 可以返回一個運算子 對應的優先順序
class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
//寫一個方法,返回對應的優先順序數字
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在該運算子" + operation);
break;
}
return result;
}
}
測試輸出
中綴表示式對應的List=[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
不存在該運算子(
不存在該運算子(
字尾表示式對應的List[1, 2, 3, +, 4, *, +, 5, -]
expression=16