摘要:而對于前綴表達式和后綴表達式的計算,則十分的簡單。由上一篇文章可知,我們目前的類所表示的,就是中綴表達式,所以我們需要提供算法,將中綴表達式轉(zhuǎn)換為前綴表達式或者后綴表達式,從而方便我們計算表達式的值。
上一篇 文章講了如何通過正則來將輸入的表達式解析為多個 Token,而這篇文章的核心在于如何對 表達式求值。
我們輸入的表達式,即我們通常見到的表達式,都是中綴表達式 —— 中綴的含義是,在表達式中,運算符是放中間的,比如 (1 + 2) * 3,運算符都是在數(shù)的中間。然而在計算機的世界里,還存在著前綴表達式和后綴表達式 —— 由名字也很容易知道,前綴表達式是將運算符放在數(shù)之前,后綴表達式是將運算符放到數(shù)之后。
表達式 | 形式 |
---|---|
中綴 | 1 + (3 - 2) * 4 / 5 |
前綴 | + 1 / * - 3 2 4 5 |
后綴 | 1 3 2 - 4 * 5 / + |
中綴表達式的劣勢在于,一旦表達式復雜化,比如多層括號嵌套同時還要注意運算符的優(yōu)先級,那么要編寫計算中綴表達式的值的代碼也十分的復雜。而對于前綴表達式和后綴表達式的計算,則十分的簡單。
以后綴表達式為例:
從左往右掃描表達式,如果遇到數(shù),那么將數(shù)入棧
如果遇到運算符,那么從棧中依次彈出兩個數(shù) n1 和 n2,使用該運算符對這兩個數(shù)進行運算(n2 op n1),將獲得的結(jié)果數(shù)入棧
重復 1 和 2 直到表達式掃描結(jié)束,那么棧中最后剩余的數(shù)便是表達式的值。
比如上面的例子,1 + (3 - 2) * 4 / 5 = 1.8,對于后綴表達式 1 3 2 - 4 * 5 / +:
當前 Token | 操作 | 棧(棧頂在左邊) |
---|---|---|
1 | 遇到數(shù)直接入棧 | 1 |
2 | 遇到數(shù)直接入棧 | 3 1 |
3 | 遇到數(shù)直接入棧 | 2 3 1 |
- | n1 = 2, n2 = 3;n2 op n1 = 3 – 2 = 1,并將 1 入棧 | 1 1 |
4 | 遇到數(shù)直接入棧 | 4 1 1 |
* | n1 = 4, n2 = 1;n2 op n1 = 4 * 1 = 4,并將 4 入棧 | 4 1 |
5 | 遇到數(shù)直接入棧 | 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 類所表示的,就是中綴表達式,所以我們需要提供算法,將中綴表達式轉(zhuǎn)換為前綴表達式或者后綴表達式,從而方便我們計算表達式的值。當然,算法的流程,我們的計算機先輩們早就想出來了,而我們只需要做出實現(xiàn)即可。
同樣以后綴表達式為例,中綴表達式轉(zhuǎn)換為后綴表達式的算法流程如下:
初始化運算符棧 S 和用來保存中間結(jié)果的列表 L
從左往右掃描中綴表達式:
遇到數(shù)時,直接將其加入到 L
遇到運算符 op 時
2.1 如果 S 為空,那么直接將 op 入棧 S 2.2 如果 S 不空,并且 S 棧頂為左括號 "(",那么將 op 入棧 S 2.3 如果 S 不空,此時 S 棧頂為運算符,如果 op 的優(yōu)先級大于 S 棧頂元素的優(yōu)先級,那么將該運算符入棧 S 2.4 否則(即 op 的優(yōu)先級小于 S 棧頂元素的優(yōu)先級),將 S 的棧頂元素彈出,并將該元素加入 L;然后轉(zhuǎn)到 2.1 繼續(xù)判斷并比較。
遇到括號時
3.1 如果是左括號 "(",直接將該左括號入棧 S 3.2 如果是右括號 ")",依次彈出 S 中的運算符,直到遇到一個左括號為止,然后將這一對括號都丟棄
將 S 中的剩余運算符依次彈出并加入 L
此時 L 中的所有的 Token 按順序即為后綴表達式
(關(guān)于中綴、前綴、后綴表達式轉(zhuǎn)換算法更詳細的內(nèi)容和例子,可以參考 前綴、中綴、后綴表達式 這篇文章,本文所寫的算法流程也是參考這篇文章而來。)
根據(jù)上面的算法,我們就不難在目前的 Expression 基礎(chǔ)上,寫出將中綴表達式轉(zhuǎn)換為后綴表達式的算法,我們將這個方法命名為 toPostfixExpr():
/** * 獲得該表達式的后綴形式 * * @return 后綴表達式 */ public Expression toPostfixExpr() { ArrayDequeS = new ArrayDeque<>(); // 運算符棧 ArrayList L = new ArrayList<>(); // 保存中間結(jié)果的列表 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 的優(yōu)先級大于運算符棧棧頂元素的優(yōu)先級 } else if (op.isHigherThan((Operator) top)) { S.push(token); } else { // op 的優(yōu)先級小于運算符棧棧頂元素的優(yōu)先級 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; } ... }
然后,根據(jù)后綴表達式,我們也很容易寫出計算表達式值的方法,我們將方法命名為 calculate():
/** * 通過后綴表達式計算表達式的值 * * @return 表達式的值 */ public Num calculate() { if (!isPostfix()) { throw new RuntimeException("請先將表達式轉(zhuǎn)為后綴表達式再計算"); } 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) { // 棧中最后剩下的不止一個數(shù),說明表達式有問題 throw new RuntimeException("錯誤的表達式"); } return (Num) stack.pop(); }
此時我們在 Num 類中定義了一個 operate 方法,用來根據(jù)運算符對兩個數(shù)進行運算:
public static final RoundingMode MODE = RoundingMode.HALF_UP; // 默認對末尾小數(shù)采用 四舍五入 public static final MathContext MATH_CONTEXT = new MathContext(6, MODE); // 無限循環(huán)時保留6位有效數(shù)字,末位四舍五入 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); }
目前來看一切都很完美 —— 但是我們忽略了一種情況,那就是輸入負數(shù)的情況。而此時存在以下兩種情況:
輸入的表達式開頭是負數(shù),比如 -1 + 2 * 3,這種情況容易解決,我們只需要在開頭補上一個 0,便能適應(yīng)現(xiàn)在的程序,比如該表達式補上 0 后就成為 0 - 1 + 2 * 3,結(jié)果一致
另一種情況就是表達式中出現(xiàn)負數(shù)了 —— 此時我們需要對負數(shù)進行特殊的標識,比如按照一般情況將負數(shù)使用括號()包圍。所以我們需要補充我們的正則表達式,讓其可以匹配類似于 (-12) 和 (-100.100) 這樣的 Token。
修改之后的 Expression((-(d*.d+|d+)) 用來匹配負數(shù)):
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) { // 負數(shù) // matcher.group(3) 提取形如 (-1.2) 中的 1.2 return new Num("-" + matcher.group(3)); } else if (matcher.group(4) != null) { // 正數(shù) 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) { // 函數(shù) Function function = new Function(matcher.group(7)); Num num = function.getResult(); // 直接計算出函數(shù)的值作為 Token return num; } } throw new RuntimeException("getToken"); // 正則無誤的情況下不會發(fā)生 } ... }
現(xiàn)在,讓我們來寫一個主類,從命令行獲得輸入并且計算輸入的表達式的值。我們將主類命名為 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); } } } } }
可以看到,我們已經(jīng)可以成功的解析表達式,并且計算表達式的值(完整的源碼在 GitHub)。
當然,我們總不能每次運行都用 Maven 來運行項目,所以我們把項目打包成 jar,然后寫個腳本來執(zhí)行這個 jar,最后將腳本加入到 PATH 中,那么便可以在命令行下直接調(diào)用。
我們將打包的 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...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/70273.html
摘要:一直以來,我的計算器都是的之后偶爾也用。因為我們要使用高精度數(shù)來代替浮點數(shù),所以的真正實現(xiàn),交給了。我們通常使用計算器的函數(shù)都是形如這樣的形式,所以我們定義函數(shù)的正則為表示參數(shù)可以有也可以沒有,即存在這樣的函數(shù)。 一直以來,我的計算器都是 Python 的 REPL(Java8 之后偶爾也用 jjs (Nashorn))。但是這些 REPL 的問題在于,在涉及到小數(shù)時,它們使用的是浮點...
摘要:虛擬網(wǎng)卡與虛擬機的生命周期一致,無法進行分離,虛擬機被銷毀時,虛擬網(wǎng)卡即被銷毀。每塊虛擬網(wǎng)卡支持綁定一個安全組,提供網(wǎng)卡級別安全控制。平臺默認提供塊虛擬網(wǎng)卡,若業(yè)務(wù)有塊以上網(wǎng)卡需求可通過綁定彈性網(wǎng)卡,為虛擬機提供多網(wǎng)絡(luò)服務(wù)。虛擬機是 UCloudStack 云平臺的核心服務(wù),提供可隨時擴展的計算能力服務(wù),包括 CPU 、內(nèi)存、操作系統(tǒng)等最基礎(chǔ)的計算組件,并與網(wǎng)絡(luò)、磁盤等服務(wù)結(jié)合提供完整的計算...
摘要:的官方網(wǎng)址為,其使用手冊網(wǎng)址為本次分享將實現(xiàn)的功能為利用爬取某個搜索詞語暫僅限英文的百度百科的介紹部分,具體的功能介紹可以參考博客爬蟲自制簡單的搜索引擎。 ??Jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文本內(nèi)容。它提供了一套非常省力的API,可通過DOM,CSS以及類似于jQuery的操作方法來取出和操作數(shù)據(jù)。Jsoup的官方網(wǎng)址為: https:...
摘要:坑一慎用方法在類中,有一個方法是,返回的是一個數(shù)組,該數(shù)組包含了所包含的方法。坑二慎用線程優(yōu)先級做并發(fā)處理線程中有屬性,表示線程的優(yōu)先級,默認值為,取值區(qū)間為。顯然,運行時環(huán)境是因操作系統(tǒng)而異的。 本文為作者原創(chuàng),轉(zhuǎn)載請注明出處。 我們都知道Java是跨平臺的,一次編譯,到處運行,本質(zhì)上依賴于不同操作系統(tǒng)下有不同的JVM。到處運行是做到了,但運行結(jié)果呢?一樣的程序,在不同的JVM上跑的...
摘要:在中處理帶小數(shù)的數(shù)據(jù)時,通常會碰到需要進行對數(shù)據(jù)進行四舍五入或者截取等操作。提供了一個的方法,很方便的幫助我們實現(xiàn)想要的操作。 在Java中處理帶小數(shù)的數(shù)據(jù)時,通常會碰到需要進行對數(shù)據(jù)進行四舍五入或者截取等操作。BigDecimal提供了一個setScale()的方法,很方便的幫助我們實現(xiàn)想要的操作。 通常用到的是下面的方法 setScale(int newScale, in...
閱讀 2302·2021-11-24 09:39
閱讀 2546·2021-11-22 15:24
閱讀 2985·2021-09-02 09:48
閱讀 3027·2021-07-26 22:01
閱讀 1443·2019-08-30 11:09
閱讀 1681·2019-08-29 18:47
閱讀 612·2019-08-29 15:40
閱讀 2139·2019-08-29 15:22