1. 程式人生 > >Java實現的表示式求值演算法(包括加減乘除以及括號運算)

Java實現的表示式求值演算法(包括加減乘除以及括號運算)

表示式求值演算法

一、表示式求值簡單說明:

1、求值表示式主要包括加減乘除四種基本運算,其實表示式可以看做由一個個二元運算構成,前一個二元運算的結果作為後一個二元運算的輸入。

        舉個例子: “1+2-4=”,“1+2”就是一個二元運算,1和2是運算元,+是運算子,它們的運算結果3作為下一個二元運算的輸入,所以下一個二元運算是“3-4”,這樣經過兩次二元運算後得出結果-1,此時碰到表示式結束符號“=”,那麼表示式運算結束,最終值為-1。當然有時候表示式不是以等號作為結束符號的,這種場景要特別注意下。

2、加減乘除四種運算子是有優先順序的,乘法和除法同級,且比加法和減法優先順序高,也就是說同級的運算順序是從左到右,高優先順序的二元運算優先執行。


        舉個例子:“1+2*3=”,“2*3”這個二元運算因為乘法的優先順序高,所以優先執行,執行結果作為+二元運算的第二個運算元。

3、括號運算其實相當於巢狀一個子表示式,而子表示式的優先順序比括號外的加減乘數二元運算高,子表示式的計算結果作為主表示式的一個運算元。

4、由上面的說明可知,表示式有兩個基本元素,那就是運算元運算子,運算子一般佔用一個字元,而運算元可能是1, 2, 3這樣的個位整數,也可能是1.0, 1.89這樣的浮點數,也可能是100, 189這樣的多位整數。所以我們在從表示式解析出運算元時要完整的解析出整個運算元。

5、表示式一般以等號作為表示式結束標記,當然有不以等號作為結束標記的場景,詳情看下面程式碼處理邏輯。

二、演算法思想簡單說明:

1、表示式由正則模式 "[0-9\\.+-/*()= ]+" 來校驗合法性。

2、表示式的運算元將push到一個數值棧中,而運算子將push到運算子棧中,解析表示式時,採用逐個讀取字元的形式,特別注意運算元是多位字元的場景,可以採用一個追加器將字元先快取起來,當完整讀取一個數值時再講數值push到數值棧中。

3、當準備push到運算子棧的當前運算子,優先順序同級於或低於棧頂運算子時,將觸發一次二元運算,參與二元運算的為運算子棧的棧頂元素以及數值棧棧頂的兩個運算元。二元運算得支援高精度運算,同時避免精度丟失問題。

        舉個例子: “2 * 3 - 1=”,在讀取到運算子 “-” 前,運算子棧中已有元素[“*

”],數值棧中有元素[“3”、“2”],因為運算子“-”的優先順序比棧頂運算子“*”的優先順序低,所以觸發二元運算 “2 * 3”,相關運算元和運算子出棧,運算結果“6”作為新的運算元push到數值棧中。特別注意,此時如果運算子棧中棧頂還有元素,那麼優先順序比對還得繼續,這是一個遞迴操作,直到運算子棧沒有元素或者當前運算子“-”的優先順序高於棧頂元素,那麼當前運算子push到運算子棧,繼續讀取表示式的下一個元素。

4、括號運算,相等於子表示式運算,當表示式解析到左括號時,將左括號push到運算子棧,當解析到右括號時,將遞迴運算整個子表示式的所有二元運算操作,直到碰到左括號才停止,此時子表示式的計算結果作為新的運算元push到數值棧中。

      舉個例子: “2 * (3 - 1*2)=”,在解析到右括號“)”前,運算子棧中已有元素[“*”,“-,“(“*”],數值棧已有元素[“2”、“1”、“3”、“2”]。

        當碰到右括號時,觸發一次二元運算,即“1*2”,運算子棧的棧頂元素“*”出棧,數值棧的棧頂元素“2”和“1”分別出棧,二元運算得出結果“2”,結果值push到數值棧。此時運算子棧中已有元素[-,“(“*”],數值棧已有元素[“2”、“3”、“2”]。

        子表示式還沒運算結束,繼續遞迴觸發二元運算,即“3-2”,運算子棧的棧頂元素“-”出棧,數值棧的棧頂元素“2”和“3”分別出棧,二元運算得出結果“1”,結果值push到數值棧。此時運算子棧中已有元素[(“*”],數值棧已有元素[“1”、“2”]。

        繼續讀取運算子棧頂元素,發現是左括號“(”,此時棧頂元素出棧,而無需觸發二元計算,此時運算子棧中已有元素[“*”],數值棧已有元素[“1”、“2”]。

        表示式繼續解析,此時發現讀取到等號“=”,遞迴觸發表示式的所有二元運算,即“2*1”,得出最終結果2,計算結束。

5、浮點數比較是否相等,因為浮點數有精度原因,所以要用精度範圍的方式,參考下面程式碼實現。

三、完整可執行演算法實現如下:


package com.begoina.zero;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 加減乘除表示式求值演算法
 * @date 2018年7月7日
 * @version 1.0
 */
public class Calculator {

    // 表示式字符合法性校驗正則模式,靜態常量化可以降低每次使用都要編譯地消耗
    private static final Pattern EXPRESSION_PATTERN = Pattern.compile("[0-9\\.+-/*()= ]+");
    
    // 運算子優先順序map
    private static final Map<String, Integer> OPT_PRIORITY_MAP = new HashMap<String, Integer>() {
        private static final long serialVersionUID = 6968472606692771458L;
        {
            put("(", 0);
            put("+", 2);
            put("-", 2);
            put("*", 3);
            put("/", 3);
            put(")", 7);
            put("=", 20);
        }
    };
    
    
    /**
     * 輸入加減乘除表示式字串,返回計算結果
     * @param expression 表示式字串
     * @return 返回計算結果
     */
    public static double executeExpression(String expression) {
    // 非空校驗
    if (null == expression || "".equals(expression.trim())) {
    throw new IllegalArgumentException("表示式不能為空!");
    }
    
    // 表示式字符合法性校驗
        Matcher matcher = EXPRESSION_PATTERN.matcher(expression);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("表示式含有非法字元!");
        }
        
        Stack<String> optStack = new Stack<>(); // 運算子棧
        Stack<BigDecimal> numStack = new Stack<>(); // 數值棧,數值以BigDecimal儲存計算,避免精度計算問題
        StringBuilder curNumBuilder = new StringBuilder(16); // 當前正在讀取中的數值字元追加器
        
        // 逐個讀取字元,並根據運算子判斷參與何種計算
        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            if (c != ' ') { // 空白字元直接丟棄掉
                if ((c >= '0' && c <= '9') || c == '.') {
                    curNumBuilder.append(c); // 持續讀取一個數值的各個字元
                } else {
                    if (curNumBuilder.length() > 0) {
                    // 如果追加器有值,說明之前讀取的字元是數值,而且此時已經完整讀取完一個數值
                        numStack.push(new BigDecimal(curNumBuilder.toString()));
                        curNumBuilder.delete(0, curNumBuilder.length());
                    }
                
                    String curOpt = String.valueOf(c);
                    if (optStack.empty()) {
                        // 運算子棧棧頂為空則直接入棧
                        optStack.push(curOpt);
                    } else {
                        if (curOpt.equals("(")) {
                            // 當前運算子為左括號,直接入運算子棧
                            optStack.push(curOpt);
                        } else if (curOpt.equals(")")) {
                            // 當前運算子為右括號,觸發括號內的字表達式進行計算
                            directCalc(optStack, numStack, true);
                        } else if (curOpt.equals("=")) {
                            // 當前運算子為等號,觸發整個表示式剩餘計算,並返回總的計算結果
                            directCalc(optStack, numStack, false);
                            return numStack.pop().doubleValue();
                        } else {
                            // 當前運算子為加減乘除之一,要與棧頂運算子比較,判斷是否要進行一次二元計算
                            compareAndCalc(optStack, numStack, curOpt);
                        }
                    }
                }
            }
        }

        // 表示式不是以等號結尾的場景
        if (curNumBuilder.length() > 0) {
    // 如果追加器有值,說明之前讀取的字元是數值,而且此時已經完整讀取完一個數值
            numStack.push(new BigDecimal(curNumBuilder.toString()));
    }
        directCalc(optStack, numStack, false);
        return numStack.pop().doubleValue();
    }
    
    /**
     * 拿當前運算子和棧頂運算子對比,如果棧頂運算子優先順序高於或同級於當前運算子,
     * 則執行一次二元運算(遞迴比較並計算),否則當前運算子入棧
     * @param optStack 運算子棧
     * @param numStack 數值棧
     * @param curOpt 當前運算子
     */
    public static void compareAndCalc(Stack<String> optStack, Stack<BigDecimal> numStack, 
    String curOpt) {
        // 比較當前運算子和棧頂運算子的優先順序
        String peekOpt = optStack.peek();
        int priority = getPriority(peekOpt, curOpt);
        if (priority == -1 || priority == 0) {
            // 棧頂運算子優先順序大或同級,觸發一次二元運算
            String opt = optStack.pop(); // 當前參與計算運算子
            BigDecimal num2 = numStack.pop(); // 當前參與計算數值2
            BigDecimal num1 = numStack.pop(); // 當前參與計算數值1
            BigDecimal bigDecimal = floatingPointCalc(opt, num1, num2);
            
            // 計算結果當做運算元入棧
            numStack.push(bigDecimal);
            
            // 運算完棧頂還有運算子,則還需要再次觸發一次比較判斷是否需要再次二元計算
            if (optStack.empty()) {
                optStack.push(curOpt);
            } else {
                compareAndCalc(optStack, numStack, curOpt);
            }
        } else {
            // 當前運算子優先順序高,則直接入棧
            optStack.push(curOpt);
        }
    }
    
    /**
     * 遇到右括號和等號執行的連續計算操作(遞迴計算)
     * @param optStack 運算子棧
     * @param numStack 數值棧
     * @param isBracket true表示為括號型別計算
     */
    public static void directCalc(Stack<String> optStack, Stack<BigDecimal> numStack, 
    boolean isBracket) {
        String opt = optStack.pop(); // 當前參與計算運算子
        BigDecimal num2 = numStack.pop(); // 當前參與計算數值2
        BigDecimal num1 = numStack.pop(); // 當前參與計算數值1
        BigDecimal bigDecimal = floatingPointCalc(opt, num1, num2);
        
        // 計算結果當做運算元入棧
        numStack.push(bigDecimal);
        
        if (isBracket) {
            if ("(".equals(optStack.peek())) {
                // 括號型別則遇左括號停止計算,同時將左括號從棧中移除
                optStack.pop();
            } else {
                directCalc(optStack, numStack, isBracket);
            }
        } else {
            if (!optStack.empty()) {
                // 等號型別只要棧中還有運算子就繼續計算
                directCalc(optStack, numStack, isBracket);
            }
        }
    }
    
    /**
     * 不丟失精度的二元運算,支援高精度計算
     * @param opt
     * @param num1
     * @param num2
     * @return
     */
    public static BigDecimal floatingPointCalc(String opt, BigDecimal bigDecimal1, 
    BigDecimal bigDecimal2) {
        BigDecimal resultBigDecimal = new BigDecimal(0);
        switch (opt) {
            case "+":
                resultBigDecimal = bigDecimal1.add(bigDecimal2);
                break;
            case "-":
                resultBigDecimal = bigDecimal1.subtract(bigDecimal2);
                break;
            case "*":
                resultBigDecimal = bigDecimal1.multiply(bigDecimal2);
                break;
            case "/":
                resultBigDecimal = bigDecimal1.divide(bigDecimal2, 10, BigDecimal.ROUND_HALF_DOWN); // 注意此處用法
                break;
            default:
                break;
        }
        return resultBigDecimal;
    }
    
    /**
     * priority = 0  表示兩個運算子同級別
     * priority = 1  第二個運算子級別高,負數則相反
     * @param opt1
     * @param opt2
     * @return
     */
    public static int getPriority(String opt1, String opt2) {
        int priority = OPT_PRIORITY_MAP.get(opt2) - OPT_PRIORITY_MAP.get(opt1);
        return priority;
    }

    /**
     * 浮點數相等比較函式
     * @param value1
     * @param value2
     * @return
     */
    private static boolean isDoubleEquals (double value1, double value2) {
    System.out.println("正確結果=" + value1 + ", 實際計算結果=" + value2);
        return Math.abs(value1 - value2) <= 0.0001;
    }
    
    public static void main(String[] args) {
    // 幾個測試資料
        System.out.println(isDoubleEquals(-39.5, executeExpression(" 2 + 3/2 * 3 - 4 *(2 + 5 -2*4/2+9) + 3 + (2*1)-3= ")));
        System.out.println(isDoubleEquals(60.3666, executeExpression("9*2+1/3*2-4+(3*2/5+3+3*6)+3*5-1+3+(4+5*1/2)=")));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199 = ")));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199  ")));
        System.out.println(isDoubleEquals(372.8, executeExpression(" 9.2 *(20-1)-1+199")));
        System.out.println(isDoubleEquals(-29, executeExpression(" 9 *(20-1)-(1+199) ")));
        System.out.println(isDoubleEquals(1.0E24, executeExpression("1000000000000*1000000000000 = ")));
    }
    
    private Calculator(){};
    
}