摘要:而對于前綴表達式和后綴表達式的計算,則十分的簡單。由上一篇文章可知,我們目前的類所表示的,就是中綴表達式,所以我們需要提供算法,將中綴表達式轉換為前綴表達式或者后綴表達式,從而方便我們計算表達式的值。
上一篇 文章講了如何通過正則來將輸入的表達式解析為多個 Token,而這篇文章的核心在于如何對 表達式求值。
我們輸入的表達式,即我們通常見到的表達式,都是中綴表達式 —— 中綴的含義是,在表達式中,運算符是放中間的,比如 (1 + 2) * 3,運算符都是在數的中間。然而在計算機的世界里,還存在著前綴表達式和后綴表達式 —— 由名字也很容易知道,前綴表達式是將運算符放在數之前,后綴表達式是將運算符放到數之后。
表達式 | 形式 |
---|---|
中綴 | 1 + (3 - 2) * 4 / 5 |
前綴 | + 1 / * - 3 2 4 5 |
后綴 | 1 3 2 - 4 * 5 / + |
中綴表達式的劣勢在于,一旦表達式復雜化,比如多層括號嵌套同時還要注意運算符的優先級,那么要編寫計算中綴表達式的值的代碼也十分的復雜。而對于前綴表達式和后綴表達式的計算,則十分的簡單。
以后綴表達式為例:
從左往右掃描表達式,如果遇到數,那么將數入棧
如果遇到運算符,那么從棧中依次彈出兩個數 n1 和 n2,使用該運算符對這兩個數進行運算(n2 op n1),將獲得的結果數入棧
重復 1 和 2 直到表達式掃描結束,那么棧中最后剩余的數便是表達式的值。
比如上面的例子,1 + (3 - 2) * 4 / 5 = 1.8,對于后綴表達式 1 3 2 - 4 * 5 / +:
當前 Token | 操作 | 棧(棧頂在左邊) |
---|---|---|
1 | 遇到數直接入棧 | 1 |
2 | 遇到數直接入棧 | 3 1 |
3 | 遇到數直接入棧 | 2 3 1 |
- | n1 = 2, n2 = 3;n2 op n1 = 3 – 2 = 1,并將 1 入棧 | 1 1 |
4 | 遇到數直接入棧 | 4 1 1 |
* | n1 = 4, n2 = 1;n2 op n1 = 4 * 1 = 4,并將 4 入棧 | 4 1 |
5 | 遇到數直接入棧 | 5 4 1 |
/ | n1 = 5, n2 = 4;n2 op n1 = 4 / 5 = 0.8,并將 0.8 入棧 | 0.8 1 |
+ | n1 = 0.8, n2 = 1;n2 op n1 = 1 + 0.8 = 1.8,并將 1.8 入棧 | 1.8 |
所以可見計算后綴表達式非常容易編碼。
由 上一篇 文章可知,我們目前的 Expression 類所表示的,就是中綴表達式,所以我們需要提供算法,將中綴表達式轉換為前綴表達式或者后綴表達式,從而方便我們計算表達式的值。當然,算法的流程,我們的計算機先輩們早就想出來了,而我們只需要做出實現即可。
同樣以后綴表達式為例,中綴表達式轉換為后綴表達式的算法流程如下:
初始化運算符棧 S 和用來保存中間結果的列表 L
從左往右掃描中綴表達式:
遇到數時,直接將其加入到 L
遇到運算符 op 時
2.1 如果 S 為空,那么直接將 op 入棧 S 2.2 如果 S 不空,并且 S 棧頂為左括號 "(",那么將 op 入棧 S 2.3 如果 S 不空,此時 S 棧頂為運算符,如果 op 的優先級大于 S 棧頂元素的優先級,那么將該運算符入棧 S 2.4 否則(即 op 的優先級小于 S 棧頂元素的優先級),將 S 的棧頂元素彈出,并將該元素加入 L;然后轉到 2.1 繼續判斷并比較。
遇到括號時
3.1 如果是左括號 "(",直接將該左括號入棧 S 3.2 如果是右括號 ")",依次彈出 S 中的運算符,直到遇到一個左括號為止,然后將這一對括號都丟棄
將 S 中的剩余運算符依次彈出并加入 L
此時 L 中的所有的 Token 按順序即為后綴表達式
(關于中綴、前綴、后綴表達式轉換算法更詳細的內容和例子,可以參考 前綴、中綴、后綴表達式 這篇文章,本文所寫的算法流程也是參考這篇文章而來。)
根據上面的算法,我們就不難在目前的 Expression 基礎上,寫出將中綴表達式轉換為后綴表達式的算法,我們將這個方法命名為 toPostfixExpr():
/** * 獲得該表達式的后綴形式 * * @return 后綴表達式 */ public Expression toPostfixExpr() { ArrayDequeS = new ArrayDeque<>(); // 運算符棧 ArrayList L = new ArrayList<>(); // 保存中間結果的列表 for (Token token : tokens) { switch (token.getType()) { case NUMBER: L.add(token); break; case OPERATOR: Operator op = (Operator) token; boolean back = true; while (back) { back = false; if (S.isEmpty()) { // 運算符棧為空 S.push(op); } else { // 運算符棧不為空 Token top = S.peek(); // 運算符棧棧頂為 "(" if (top.isBracket() && ((Bracket) top).isLeft()) { S.push(op); // op 的優先級大于運算符棧棧頂元素的優先級 } else if (op.isHigherThan((Operator) top)) { S.push(token); } else { // op 的優先級小于運算符棧棧頂元素的優先級 L.add(S.pop()); back = true; // 回到 while } } } break; case BRACKET: if (((Bracket) token).isLeft()) { S.push(token); } else { for (Token t = S.pop(); !t.isBracket(); t = S.pop()) { L.add(t); } } break; } } while (!S.isEmpty()) { L.add(S.pop()); } return new Expression(L, true); // true 表示該表達式為后綴表達式 }
此時我們往 Expression 中添加了一個 boolean 字段 postfix,用來標識該表達式是否為后綴表達式,postfix 默認為 false,如果為 true 則表明該表達式是后綴表達式。
public class Expression { ... private final Listtokens; // 該表達式中的所有 Token private final boolean postfix; // 該表達式是否為后綴表達式的標識 public Expression(List tokens, boolean postfix) { this.tokens = tokens; this.postfix = postfix; } /** * 該表達式是否為后綴表達式 * * @return 如果該表達式為后綴表達式返回 true,否則返回 false */ public boolean isPostfix() { return postfix; } ... }
然后,根據后綴表達式,我們也很容易寫出計算表達式值的方法,我們將方法命名為 calculate():
/** * 通過后綴表達式計算表達式的值 * * @return 表達式的值 */ public Num calculate() { if (!isPostfix()) { throw new RuntimeException("請先將表達式轉為后綴表達式再計算"); } ArrayDequestack = new ArrayDeque<>(); for (Token token : tokens) { if (token.isNumber()) { stack.push(token); } else { Num n1 = (Num) stack.pop(); Num n2 = (Num) stack.pop(); Operator op = (Operator) token; Num result = n2.operate(op, n1); stack.push(result); } } if (stack.size() != 1) { // 棧中最后剩下的不止一個數,說明表達式有問題 throw new RuntimeException("錯誤的表達式"); } return (Num) stack.pop(); }
此時我們在 Num 類中定義了一個 operate 方法,用來根據運算符對兩個數進行運算:
public static final RoundingMode MODE = RoundingMode.HALF_UP; // 默認對末尾小數采用 四舍五入 public static final MathContext MATH_CONTEXT = new MathContext(6, MODE); // 無限循環時保留6位有效數字,末位四舍五入 public Num operate(Operator op, Num other) { BigDecimal result = null; switch (op.value()) { case "+": result = value.add(other.value); break; case "-": result = value.subtract(other.value); break; case "*": result = value.multiply(other.value); break; case "/": result = value.divide(other.value, MATH_CONTEXT); break; } if (result == null) { throw new RuntimeException(String.format( "operate 方法出錯:%s %s %s", value, op.text(), other.value)); } return new Num(result); }
目前來看一切都很完美 —— 但是我們忽略了一種情況,那就是輸入負數的情況。而此時存在以下兩種情況:
輸入的表達式開頭是負數,比如 -1 + 2 * 3,這種情況容易解決,我們只需要在開頭補上一個 0,便能適應現在的程序,比如該表達式補上 0 后就成為 0 - 1 + 2 * 3,結果一致
另一種情況就是表達式中出現負數了 —— 此時我們需要對負數進行特殊的標識,比如按照一般情況將負數使用括號()包圍。所以我們需要補充我們的正則表達式,讓其可以匹配類似于 (-12) 和 (-100.100) 這樣的 Token。
修改之后的 Expression((-(d*.d+|d+)) 用來匹配負數):
public class Expression { private static final String REG_EXPR = "s*(((-(d*.d+|d+)))|(d*.d+|d+)|(+|-|*|/)|((|))|([A-Za-z]+(.*)))s*"; private static final Pattern PATTERN = Pattern.compile(REG_EXPR); ... private static Token getToken(Matcher matcher) { // matcher.group(0) 匹配整個正則,matcher.group(1) 匹配第一個括號 String m = matcher.group(1); if (m != null) { if (matcher.group(2) != null) { // 負數 // matcher.group(3) 提取形如 (-1.2) 中的 1.2 return new Num("-" + matcher.group(3)); } else if (matcher.group(4) != null) { // 正數 return new Num(matcher.group(4)); } else if (matcher.group(5) != null) { // 運算符 return new Operator(matcher.group(5).charAt(0)); } else if (matcher.group(6) != null) { // 括號 return new Bracket(matcher.group(6).charAt(0)); } else if (matcher.group(7) != null) { // 函數 Function function = new Function(matcher.group(7)); Num num = function.getResult(); // 直接計算出函數的值作為 Token return num; } } throw new RuntimeException("getToken"); // 正則無誤的情況下不會發生 } ... }
現在,讓我們來寫一個主類,從命令行獲得輸入并且計算輸入的表達式的值。我們將主類命名為 Launcher:
public class Launcher { public static void main(String[] args) throws Exception { System.out.println("歡迎使用你的計算器(輸入 e(xit) 退出)"); try (Reader in = new InputStreamReader(System.in); BufferedReader reader = new BufferedReader(in)) { String line; while (true) { System.out.print("> "); line = reader.readLine(); if (null == line || "e".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { break; } else if (line.isEmpty()) { continue; } try { Expression expr = Expression.of(line); Expression postfixExpr = expr.toPostfixExpr(); Num result = postfixExpr.calculate(); System.out.println(result); } catch (ArithmeticException ex) { System.out.println("運算錯誤:" + ex.getMessage()); } catch (RuntimeException ex) { System.out.println("運行錯誤:" + ex.getMessage()); // ex.printStackTrace(System.err); } } } } }
可以看到,我們已經可以成功的解析表達式,并且計算表達式的值(完整的源碼在 GitHub)。
當然,我們總不能每次運行都用 Maven 來運行項目,所以我們把項目打包成 jar,然后寫個腳本來執行這個 jar,最后將腳本加入到 PATH 中,那么便可以在命令行下直接調用。
我們將打包的 jar 命名為 mcalc.jar (打包的配置可參考 pom.xml),然后寫個簡單的腳本。比如,在 Windows 上,寫個 mcalc.bat:
@echo off :: %~dp0 表示當前批處理文件所在目錄的路徑 set DIR_PATH=%~dp0 java -jar %DIR_PATH%mcalc.jar
然后將 mcalc.bat 和 mcalc.jar 放到同一個文件夾下,然后將這個文件夾的路徑加入到 PATH。此時在命令行中直接輸入 mcalc,便可以進入程序:
參考:
http://blog.csdn.net/antineut...
http://blog.csdn.net/yu757371...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70273.html
摘要:一直以來,我的計算器都是的之后偶爾也用。因為我們要使用高精度數來代替浮點數,所以的真正實現,交給了。我們通常使用計算器的函數都是形如這樣的形式,所以我們定義函數的正則為表示參數可以有也可以沒有,即存在這樣的函數。 一直以來,我的計算器都是 Python 的 REPL(Java8 之后偶爾也用 jjs (Nashorn))。但是這些 REPL 的問題在于,在涉及到小數時,它們使用的是浮點...
摘要:虛擬網卡與虛擬機的生命周期一致,無法進行分離,虛擬機被銷毀時,虛擬網卡即被銷毀。每塊虛擬網卡支持綁定一個安全組,提供網卡級別安全控制。平臺默認提供塊虛擬網卡,若業務有塊以上網卡需求可通過綁定彈性網卡,為虛擬機提供多網絡服務。虛擬機是 UCloudStack 云平臺的核心服務,提供可隨時擴展的計算能力服務,包括 CPU 、內存、操作系統等最基礎的計算組件,并與網絡、磁盤等服務結合提供完整的計算...
摘要:的官方網址為,其使用手冊網址為本次分享將實現的功能為利用爬取某個搜索詞語暫僅限英文的百度百科的介紹部分,具體的功能介紹可以參考博客爬蟲自制簡單的搜索引擎。 ??Jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文本內容。它提供了一套非常省力的API,可通過DOM,CSS以及類似于jQuery的操作方法來取出和操作數據。Jsoup的官方網址為: https:...
摘要:坑一慎用方法在類中,有一個方法是,返回的是一個數組,該數組包含了所包含的方法。坑二慎用線程優先級做并發處理線程中有屬性,表示線程的優先級,默認值為,取值區間為。顯然,運行時環境是因操作系統而異的。 本文為作者原創,轉載請注明出處。 我們都知道Java是跨平臺的,一次編譯,到處運行,本質上依賴于不同操作系統下有不同的JVM。到處運行是做到了,但運行結果呢?一樣的程序,在不同的JVM上跑的...
摘要:在中處理帶小數的數據時,通常會碰到需要進行對數據進行四舍五入或者截取等操作。提供了一個的方法,很方便的幫助我們實現想要的操作。 在Java中處理帶小數的數據時,通常會碰到需要進行對數據進行四舍五入或者截取等操作。BigDecimal提供了一個setScale()的方法,很方便的幫助我們實現想要的操作。 通常用到的是下面的方法 setScale(int newScale, in...
閱讀 2299·2021-11-24 09:39
閱讀 2543·2021-11-22 15:24
閱讀 2982·2021-09-02 09:48
閱讀 3020·2021-07-26 22:01
閱讀 1439·2019-08-30 11:09
閱讀 1677·2019-08-29 18:47
閱讀 610·2019-08-29 15:40
閱讀 2138·2019-08-29 15:22