一、程序要求
解析一般數學算式,實現簡單的帶括號的加減乘除運算。
二、基本思路
前面兩篇介紹了直接解析字符串和用數組容器輔助解析的兩種方式,這次再介紹最常用的解析算法——解析后綴表達式(逆波蘭表達式)。
三、逆波蘭表達式及其得到算法
1、逆波蘭表達式
也即后綴表達式,指的是不包含括號,運算符放在兩個運算對象的后面,所有的計算按運算符出現的順序,嚴格從左向右進行(不再考慮運算符的優先規則)。(摘自百度),既然沒了運算符的優先規則,那么計算機解析起來自然容易的多。
對於我們常見的表達式,稱為中綴表達式,每個中綴表達式都有對應的后綴表達式。如:
中綴表達式:-2*(1+6/3)+4
后綴表達式:-2 1 6 3 / + * 4 +(這里為了區分負號和減號,我在數字與數字、數字與符號之間都加了空格,至於怎么從中綴表達式得到后綴表達式,后面有介紹及參考程序)
而在解析后綴表達式時,只需要遵守以下原則即可:
從左往右遍歷
遇到數字直接放入容器
遇到運算符,將最后兩個數字取出,進行該運算,將結果再放入容器
遍歷結束后,容器中的數字即為運算結果
按這個過程走下來,自然而然的想到用棧是最合適的。
現只需想辦法由輸入的中綴表達式轉為后綴表達式即可完成解析。
2、由中綴表達式得到后綴表達式的算法
由中綴表達式得到后綴表達式,只要遵守以下步驟即可:
首先設置運算符的優先級(這樣設置也是為了簡化程序):
”null” 棧頂若為空,假設優先級為0
“(” 優先級設為1
“+-” 優先級設為2
“*/” 優先級設為3
從左向右遍歷中綴表達式
遇到數字直接輸出
遇到符號
遇到左括號,直接壓棧
遇到右括號,彈棧輸出直到彈出左括號(左括號不輸出)
遇到運算符,比較棧頂符號,若該運算符優先級大於棧頂,直接壓棧;若小於棧頂,彈棧輸出直到大於棧頂,然后將改運算符壓棧。
最后將符合棧彈棧並輸出
現根據這個原則,手動模擬一遍轉換過程:
還是以-2*(1+6/3)+4為例
四、代碼一
環境:
- Eclipse Java EE IDE(Version: Oxygen.1a Release (4.7.1a))
- jdk1.8.0_131
先寫一個最基本的兩位數四則運算方法,比較簡單,沒有寫注釋:
private static double doubleCal(double a1, double a2, char operator) throws Exception { switch (operator) { case '+': return a1 + a2; case '-': return a1 - a2; case '*': return a1 * a2; case '/': return a1 / a2; default: break; } throw new Exception("illegal operator!"); }
寫一個獲得優先級的方法:
private static int getPriority(String s) throws Exception { if(s==null) return 0; switch(s) { case "(":return 1; case "+":; case "-":return 2; case "*":; case "/":return 3; default:break; } throw new Exception("illegal operator!"); }
將中綴表達式轉變為后綴表達式:
private static String toSufExpr(String expr) throws Exception { System.out.println("將"+expr+"解析為后綴表達式..."); /*返回結果字符串*/ StringBuffer sufExpr = new StringBuffer(); /*盛放運算符的棧*/ Stack<String> operator = new Stack<String>(); operator.push(null);//在棧頂壓人一個null,配合它的優先級,目的是減少下面程序的判斷 /* 將expr打散分散成運算數和運算符 */ Pattern p = Pattern.compile("(?<!\\d)-?\\d+(\\.\\d+)?|[+\\-*/()]");//這個正則為匹配表達式中的數字或運算符 Matcher m = p.matcher(expr); while (m.find()) { String temp = m.group(); if (temp.matches("[+\\-*/()]")) { //是運算符 if (temp.equals("(")) { //遇到左括號,直接壓棧 operator.push(temp); System.out.println("'('壓棧"); } else if (temp.equals(")")) { //遇到右括號,彈棧輸出直到彈出左括號(左括號不輸出) String topItem = null; while (!(topItem = operator.pop()).equals("(")) { System.out.println(topItem+"彈棧"); sufExpr.append(topItem+" "); System.out.println("輸出:"+sufExpr); } } else {//遇到運算符,比較棧頂符號,若該運算符優先級大於棧頂,直接壓棧;若小於棧頂,彈棧輸出直到大於棧頂,然后將改運算符壓棧。 while(getPriority(temp) <= getPriority(operator.peek())) { sufExpr.append(operator.pop()+" "); System.out.println("輸出sufExpr:"+sufExpr); } operator.push(temp); System.out.println("\""+temp+"\""+"壓棧"); } }else {//遇到數字直接輸出 sufExpr.append(temp+" "); System.out.println("輸出sufExpr:"+sufExpr); } } String topItem = null;//最后將符合棧彈棧並輸出 while(null != (topItem = operator.pop())) { sufExpr.append(topItem+" "); } return sufExpr.toString(); }
解析中綴表達式的方法:
public static String getResult(String expr) throws Exception { String sufExpr = toSufExpr(expr);// 轉為后綴表達式 System.out.println("開始計算后綴表達式..."); /* 盛放數字棧 */ Stack<Double> number = new Stack<Double>(); /* 這個正則匹配每個數字和符號 */ Pattern p = Pattern.compile("-?\\d+(\\.\\d+)?|[+\\-*/]"); Matcher m = p.matcher(sufExpr); while (m.find()) { String temp = m.group(); if (temp.matches("[+\\-*/]")) {// 遇到運算符,將最后兩個數字取出,進行該運算,將結果再放入容器 System.out.println("符號"+temp); double a1 = number.pop(); double a2 = number.pop(); double res = doubleCal(a2, a1, temp.charAt(0)); number.push(res); System.out.println(a2 + "和" + a1 + "彈棧,並計算" + a2 + temp + a1); System.out.println("數字棧:" + number); } else {// 遇到數字直接放入容器 number.push(Double.valueOf(temp)); System.out.println("數字棧:" + number); } } return number.pop() + ""; }
主方法,以-3.5*(4.5-(4+(-1-1/2)))測試
public static void main(String[] args) throws Exception { String str = "-3.5*(4.5-(4+(-1-1/2)))"; System.out.println(getResult(str)); }
五、執行結果
六、簡化過程分析
根據這個算法,在不需要解出后綴表達式的情況下,還可以將代碼進一步簡化。
在解析的過程的中,我們只需要按照以下原則:
使用兩個棧,一個數字棧,一個符號棧
從左往右遍歷表達式字符串
遇到數字,直接壓入數字棧
遇到符號
遇到左括號,直接入符號棧
遇到右括號,”符號棧彈棧取棧頂符號b,數字棧彈棧取棧頂數字a1,數字棧彈棧取棧頂數字a2,計算a2 b a1 ,將結果壓入數字棧”,重復引號步驟至取棧頂為左括號,將左括號彈出
遇到運算符,1)若該運算符的優先級大於棧頂元素的優先級,直接入符號棧。2)若小於,”符號棧彈棧取棧頂符號b,數字棧彈棧取棧頂數字a1,數字棧彈棧取棧頂數字a2,計算a2 b a1 ,將結果壓入數字棧”,重復引號步驟至該運算符的優先級大於符號棧頂元素的優先級,然后將該符號入符號棧
遍歷結束后,”符號棧彈棧取棧頂符號b,數字棧彈棧取棧頂數字a1,數字棧彈棧取棧頂數字a2,計算a2 b a1 ,將結果壓入數字棧”,重復引號步驟至符號棧無符號(或數字棧只有一個元素),則數字棧的元素為運算結果
七、代碼二
環境:
Eclipse Java EE IDE(Version: Oxygen.1a Release (4.7.1a))
jdk1.8.0_131
先寫一個最基本的兩位數四則運算方法,比較簡單,沒有寫注釋:
private static double doubleCal(double a1, double a2, char operator) throws Exception { switch (operator) { case '+': return a1 + a2; case '-': return a1 - a2; case '*': return a1 * a2; case '/': return a1 / a2; default: break; } throw new Exception("illegal operator!"); }
寫一個獲得優先級的方法:
private static int getPriority(String s) throws Exception { if(s==null) return 0; switch(s) { case "(":return 1; case "+":; case "-":return 2; case "*":; case "/":return 3; default:break; } throw new Exception("illegal operator!"); }
解析表達式:
public static String getResult(String expr) throws Exception { System.out.println("計算"+expr); /*數字棧*/ Stack<Double> number = new Stack<Double>(); /*符號棧*/ Stack<String> operator = new Stack<String>(); operator.push(null);// 在棧頂壓人一個null,配合它的優先級,目的是減少下面程序的判斷 /* 將expr打散為運算數和運算符 */ Pattern p = Pattern.compile("(?<!\\d)-?\\d+(\\.\\d+)?|[+\\-*/()]");// 這個正則為匹配表達式中的數字或運算符 Matcher m = p.matcher(expr); while(m.find()) { String temp = m.group(); if(temp.matches("[+\\-*/()]")) {//遇到符號 if(temp.equals("(")) {//遇到左括號,直接入符號棧 operator.push(temp); System.out.println("符號棧更新:"+operator); }else if(temp.equals(")")){//遇到右括號,"符號棧彈棧取棧頂符號b,數字棧彈棧取棧頂數字a1,數字棧彈棧取棧頂數字a2,計算a2 b a1 ,將結果壓入數字棧",重復引號步驟至取棧頂為左括號,將左括號彈出 String b = null; while(!(b = operator.pop()).equals("(")) { System.out.println("符號棧更新:"+operator); double a1 = number.pop(); double a2 = number.pop(); System.out.println("數字棧更新:"+number); System.out.println("計算"+a2+b+a1); number.push(doubleCal(a2, a1, b.charAt(0))); System.out.println("數字棧更新:"+number); } System.out.println("符號棧更新:"+operator); }else {//遇到運算符,滿足該運算符的優先級大於棧頂元素的優先級壓棧;否則計算后壓棧 while(getPriority(temp) <= getPriority(operator.peek())) { double a1 = number.pop(); double a2 = number.pop(); String b = operator.pop(); System.out.println("符號棧更新:"+operator); System.out.println("數字棧更新:"+number); System.out.println("計算"+a2+b+a1); number.push(doubleCal(a2, a1, b.charAt(0))); System.out.println("數字棧更新:"+number); } operator.push(temp); System.out.println("符號棧更新:"+operator); } }else {//遇到數字,直接壓入數字棧 number.push(Double.valueOf(temp)); System.out.println("數字棧更新:"+number); } } while(operator.peek()!=null) {//遍歷結束后,符號棧數字棧依次彈棧計算,並將結果壓入數字棧 double a1 = number.pop(); double a2 = number.pop(); String b = operator.pop(); System.out.println("符號棧更新:"+operator); System.out.println("數字棧更新:"+number); System.out.println("計算"+a2+b+a1); number.push(doubleCal(a2, a1, b.charAt(0))); System.out.println("數字棧更新:"+number); } return number.pop()+""; }
主方法,以-3.5*(4.5-(4+(-1-1/2)))測試
public static void main(String[] args) throws Exception { String str = "-3.5*(4.5-(4+(-1-1/2)))"; System.out.println(getResult(str)); }