本文已在本人微信公衆號「碼農小阿飛」上發佈,打開微信搜索「碼農小阿飛」,或者掃描文章結尾的二維碼,進入公衆號並關注,就能夠在第一時間收到個人推文!
前段時間,在逛論壇的時候,看到一個比較有趣的提問:怎麼用代碼實現一個程序,能夠根據用戶動態輸入的傳統算術表達式,去解析並計算這個表達式,最後,給用戶返回一個計算結果?固然了,這個算術表達式比較簡單,運算操做符只有+-*/()
。java
例如:用戶輸入的字符串表達式爲5+2*(1+3*(5-1*2))
,程序運行結束給用戶返回一個計算結果25
。那是怎麼計算得出這麼一個結果的呢?正則表達式
「先乘除後加減,有小括號要先計算小括號裏的」,幼兒園小朋友可能都知道的邏輯。但是,對計算機來講,只能經過循環和判斷等方式來解決問題,怎麼用代碼來讓計算機來根據這個規則去運算呢?express
由於算術表達式的計算是有前後順序關係,必須先找到表達式中優先級高的運算操做,先計算得出結果,再考慮優先級較低的運算操做,這就涉及到一個尋找匹配的過程,所以天然而然可以想到的就是用正則表達式。編程
在正則表達式中,用\\([^\\(\\)]*\\)
匹配一對沒有嵌套的單層括號對,這應該好理解,一對小括號,中間有一串表達式,可是再也不嵌套其餘小括號。用\\-?\\d+(\\*|\\/)\\-?\\d+
匹配乘除運算,用\\-?\\d+(\\+|\\-)\\-?\\d+
匹配加減運算。這也好理解,經過+-*/
鏈接的兩個數值串,固然,數值多是負數因此數值的模式串爲\\-?\\d+
。數組
最後,只要依次判斷這三個正則表達式在當前表達式中是否存在,若是存在,則把匹配的內容取出來作相應的計算操做,並將結果替換原來匹配出來的子串。微信
舉個簡單的例子,但凡能被正則表達式\\-?\\d+\\*\\-?\\d+
匹配的字符串必定知足格式a*b
(a和b都是數值,固然多是負的),咱們只須要以*
爲分隔符,將子串a和b分隔提取出來,再將子串a和b都轉換成int整形進行相應的計算。數據結構
if (expression.matches("\\-?\\d+\\*\\-?\\d+")) { String[] split = expression.split("\\*"); int value1 = Integer.parseInt(split[0]); int value2 = Integer.parseInt(split[1]); return value1 * value2; }
最後再用這個計算結果替換原表達式中的a*b
,除法、加法、減法也是這麼處理,一步一步以此類推,先匹配再運算,直到表達式再也匹配不到乘除運算和加減運算,就能夠輸出結果了。工具
這是我用正則表達式實現的程序,根據輸入打印的結果:
能夠看出來,和咱們常規計算算術表達式時的思路同樣。ui
固然,實際代碼操做中還有很多細節須要處理,由於篇幅有限,也不是本篇推崇的實現方案,這裏提出一種思路,就不在此處作代碼展現,對正則表達式實現感興趣或者想要去了解的朋友能夠私信我,一塊兒討論。spa
雖然,看起來輕鬆寫意,寥寥幾語就把實現方案描繪出來,但用正則表達式匹配字符串整體上仍是比較繁瑣,效率也比較低,很容易在寫匹配模式串的時候,由於考慮不到的狀況,致使整個表達式沒法解開,並且在拓展更多操做符的時候,也不是太方便。那到底會不會存在更簡潔優雅的方式去實現呢?答案是確定的,那就是今天我要推薦給大夥兒的後綴表達式,一個爲計算機執行算術運算而生的表達式。
後綴大夥兒應該都知道,在英語中能夠根據單詞的後綴區分詞性,在計算機中能夠根據文件名的後綴區分文件類型,但是,後綴表達式又是什麼呢?它們之間又有着什麼樣的聯繫呢?
先來看一個表達式:1 2 3 + 4 * + 5 -
。你沒看錯,我也沒有寫錯,這的的確確是一個表達式,這是就是算術表達式1+((2+3)*4)-5
對應的後綴表達式,由於表達式中操做運算符都位於須要進行運算操做的數值的後邊(右側),於是得名後綴表達式。
後綴表達式有一個運算規則:從左往右依次遍歷後綴表達式,若是遍歷到的元素是數值的話,將數值入棧到一個數值棧中。若是遍歷到的元素是運算符的話,取出數值棧棧頂的前兩個數值,以"次頂元素 運算符 棧頂元素"的位置關係,作相應的算術運算,並把運算的結果入棧到數值棧中,直到遍歷到後綴表達式的末端,再將棧頂的元素取出,即是運算的結果。咱們先根據規則,運行上述後綴表達式,運行過程步驟以下圖所示:
/*** * 解析計算給定的後綴表達式 * * @param rpnExpression 後綴表達式 * */ public int compute(String[] rpnExpression) { Objects.requireNonNull(rpnExpression, "後綴表達式爲空"); Stack<Integer> stack = new Stack<>(); int value1, value2; for (String string : rpnExpression) { switch (string) { case "+": { if (2 > stack.size()) { throw new RuntimeException("解析異常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 + value2); break; } case "-": { if (2 > stack.size()) { throw new RuntimeException("解析異常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 - value2); break; } case "*": { if (2 > stack.size()) { throw new RuntimeException("解析異常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 * value2); break; } case "/": { if (2 > stack.size()) { throw new RuntimeException("解析異常"); } value2 = stack.pop(); value1 = stack.pop(); stack.push(value1 / value2); break; } default: { stack.push(Integer.parseInt(string)); break; } } } if (1 != stack.size()) { throw new RuntimeException("解析異常"); } return stack.pop(); }
能夠發現,運算的結果是和表達式1+((2+3)*4)-5
的結果同樣。
對比傳統的算術表達式運算,後綴表達式確實知足運算符的前後順序,而且計算機執行起來更加簡潔方便,只要簡單的從左往右遍歷表達式,就能計算出結果,避免了使用正則表達式去處理時由於匹配優先級各類字符串匹配的過程。那究竟是怎麼從一個算術表達式推導出一個後綴表達式的呢?
一樣,後綴表達式的推導也一樣有一個規則:這裏須要初始化兩個輔助工具一個隊列和一個堆棧,分別是後綴表達式輸出隊列(先進先出)和操做符暫存棧(先進後出)。
從左往右依次遍歷算術表達式,若是遍歷到的元素是數值的話,直接入隊到輸出隊列中,若是遍歷到的元素是操做符的話,狀況比較複雜一些,須要考慮這些操做運算符的優先級:
若是當前遍歷到的操做符是+-
的話,由於優先級相對較低,只有在操做符堆棧爲空或者棧頂操做符爲(
的時候才能入棧。若是棧頂操做符的優先級大於或等於當前操做符的話,則將棧頂操做符出棧,併入隊到後綴表達式輸出隊列。在棧頂操做符出棧後,當前操做符繼續和新的棧頂操做符比較,以此類推,直到達到入棧標準。
若是當前遍歷到的操做符是*/
的話,思想和上述+-
同樣,可是由於*/
的優先級相對較高,因此入棧的條件相對較低,只要堆棧爲空,或者只要棧頂元素不是*/
都能入棧。不然,將棧頂操做符出棧,併入隊到後綴表達式輸出隊列。在棧頂操做符出棧後,當前操做符繼續和新的棧頂操做符比較,以此類推,直到達到入棧標準。
這裏最特殊的當屬操做符()
,若是當前遍歷到的操做符是(
的話,不論什麼狀況,直接入棧。若是當前遍歷到的操做符爲)
的話,操做符堆棧內距離棧頂最近的那個操做符(
和棧頂組成的一個區間內全部的操做符,依次出棧,併入隊到後綴表達式輸出隊列。出棧完畢後,將棧頂的操做符(
出棧,並捨去。這也就是後綴表達式明明沒有小括號,卻一樣能實現本來算術表達式中小括號內的運算優先的保證。
/*** * 將中綴表達式轉換爲後綴表達式 * */ public String[] parse2rpn(String[] expressionArray) { Objects.requireNonNull(expressionArray, "算術表達式數組爲空"); Queue<String> queue = new LinkedList<>(); Stack<String> stack = new Stack<>(); for (String string : expressionArray) { if (!"+".equals(string) && !"-".equals(string) && !"*".equals(string) && !"/".equals(string) && !"(".equals(string) && !")".equals(string)) { // 非操做符直接輸出到隊列 queue.offer(string); continue; } if ("+".equals(string) || "-".equals(string)) { while (true) { // 加減符號只有在空棧,或者棧頂操做符爲'('的狀況下可以入棧 if ((0 >= stack.size() || "(".equals(stack.peek()))) { stack.push(string); break; } else { queue.offer(stack.pop()); } } continue; } if ("*".equals(string) || "/".equals(string)) { while (true) { // 乘除符號只要棧頂符號不是乘除符號,都能入棧 if ((0 >= stack.size() || (!"*".equals(stack.peek()) && !"/".equals(stack.peek())))) { stack.push(string); break; } else { queue.offer(stack.pop()); } } continue; } // 左括號任何狀況直接入棧 if ("(".equals(string)) { stack.push(string); continue; } while (true) { if (0 >= stack.size()) { throw new RuntimeException("表達式異常"); } if (!"(".equals(stack.peek())) { queue.offer(stack.pop()); } else { stack.pop(); break; } } } while (0 < stack.size()) { queue.offer(stack.pop()); } return queue.toArray(new String[0]); }
後綴表達式在推導過程當中,巧妙的運用了數據結構中棧先進後出的特性,將優先級較高的操做符放置在棧頂,在出棧的時候,棧頂的操做符優先入隊到輸出隊列中,這也就知足了從左往右遍歷後綴表達式的時候優先級較高的操做符在左側優先計算。所以,在後續運行的時候就不須要再去考慮優先級問題,從左往右執行運算操做就行。
能夠發現,實現從傳統算術表達式也到後綴表達式的轉換並不難,雖然可讀性降低了,可是卻能使計算機理解起來簡單,下降編寫程序的複雜性,提升運行的效率。
文章到這裏也該結束了,若是以爲這篇文章對你瞭解後綴表達式或者在平常解決問題有幫助的話,不勝榮幸。固然,若是對文章有什麼疑問,又或者是文章中有什麼地方有誤或者解釋不當,請在評論區留言,一塊兒探討。
關注我,帶你去看編程世界中那些有趣的發現。