首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 實現四則運算表達式解析器:遞歸下降與棧算法雙方案

     編輯:Java 相關知識

前言

四則運算解析是編譯原理經典實踐,涉及詞法分析、語法分析、運算符優先級、括號嵌套。本文提供遞歸下降解析器與雙棧算法兩種工業級實現,支援 + - * / () 與小數運算。

一、遞歸下降解析器(推薦:結構清晰、易擴展)

1.1 文法定義(EBNF)

Expression  = Term { ('+' | '-') Term }*
Term        = Factor { ('*' | '/') Factor }*
Factor      = Number | '(' Expression ')' | ('+' | '-') Factor  // 支援一元 +/- 
Number      = Digit+ ('.' Digit+)?

1.2 核心實現

public class RecursiveDescentParser {
    private final String input;
    private int pos = 0;
    private char currentChar;

    public RecursiveDescentParser(String input) {
        this.input = input.replaceAll("\\s+", ""); // 移除空白
        this.currentChar = input.isEmpty() ? '\0' : input.charAt(0);
    }

    public BigDecimal parse() {
        BigDecimal result = expression();
        if (pos < input.length()) throw new ParseException("Unexpected char at " + pos);
        return result;
    }

    // Expression → Term { (+|-) Term }*
    private BigDecimal expression() {
        BigDecimal result = term();
        while (match('+', '-')) {
            char op = currentChar;
            advance();
            BigDecimal right = term();
            result = op == '+' ? result.add(right) : result.subtract(right);
        }
        return result;
    }

    // Term → Factor { (*|/) Factor }*
    private BigDecimal term() {
        BigDecimal result = factor();
        while (match('*', '/')) {
            char op = currentChar;
            advance();
            BigDecimal right = factor();
            if (op == '*') result = result.multiply(right);
            else {
                if (right.compareTo(BigDecimal.ZERO) == 0) 
                    throw new ArithmeticException("Division by zero");
                result = result.divide(right, 10, RoundingMode.HALF_UP);
            }
        }
        return result;
    }

    // Factor → Number | '(' Expression ')' | (+|-) Factor
    private BigDecimal factor() {
        if (match('+', '-')) {
            char op = currentChar;
            advance();
            BigDecimal val = factor();
            return op == '-' ? val.negate() : val;
        }
        if (match('(')) {
            advance(); // consume '('
            BigDecimal result = expression();
            if (!match(')')) throw new ParseException("Missing ')'");
            advance(); // consume ')'
            return result;
        }
        return number();
    }

    // Number → Digit+ ('.' Digit+)?
    private BigDecimal number() {
        StringBuilder sb = new StringBuilder();
        while (matchDigit()) {
            sb.append(currentChar);
            advance();
        }
        if (match('.')) {
            sb.append('.');
            advance();
            while (matchDigit()) {
                sb.append(currentChar);
                advance();
            }
        }
        if (sb.length() == 0) throw new ParseException("Expected number at " + pos);
        return new BigDecimal(sb.toString());
    }

    // 工具方法
    private boolean match(char... chars) {
        for (char c : chars) if (currentChar == c) return true;
        return false;
    }
    private boolean matchDigit() { return currentChar >= '0' && currentChar <= '9'; }
    private void advance() { currentChar = ++pos < input.length() ? input.charAt(pos) : '\0'; }
}

1.3 測試驗證

public static void main(String[] args) {
    String[] cases = {
        "2+3*4",           // 14
        "(2+3)*4",         // 20
        "10/3",            // 3.3333333333
        "-5+3",            // -2
        "2*(-3+4)",        // 2
        "3.14*2",          // 6.28
        "((1+2)*3-4)/5"    // 1
    };
    for (String expr : cases) {
        try {
            BigDecimal result = new RecursiveDescentParser(expr).parse();
            System.out.printf("%-20s = %s%n", expr, result.stripTrailingZeros().toPlainString());
        } catch (Exception e) {
            System.err.printf("%-20s ERROR: %s%n", expr, e.getMessage());
        }
    }
}

二、雙棧算法(經典:Dijkstra Shunting Yard 變體)

2.1 算法邏輯

  • 操作數棧:存儲數值
  • 運算符棧:存儲 + - * / ( )
  • 優先級:( > * / > + - > )
  • 規則:當前運算符優先級 ≤ 棧頂運算符 → 出棧計算;> → 入棧

2.2 核心實現

public class TwoStackCalculator {
    private static final Map<Character, Integer> PRECEDENCE = Map.of(
        '(', 3, '*', 2, '/', 2, '+', 1, '-', 1, ')', 0
    );

    public BigDecimal calculate(String expression) {
        Deque<BigDecimal> numStack = new ArrayDeque<>();
        Deque<Character> opStack = new ArrayDeque<>();
        
        String expr = expression.replaceAll("\\s+", "");
        StringBuilder numBuf = new StringBuilder();
        
        for (int i = 0; i < expr.length(); i++) {
            char ch = expr.charAt(i);
            
            if (Character.isDigit(ch) || ch == '.') {
                numBuf.append(ch);
                continue;
            }
            
            // 遇到運算符,先壓入緩衝的數字
            if (numBuf.length() > 0) {
                numStack.push(new BigDecimal(numBuf.toString()));
                numBuf.setLength(0);
            }
            
            if (ch == '(') {
                opStack.push(ch);
            } else if (ch == ')') {
                while (!opStack.isEmpty() && opStack.peek() != '(') 
                    compute(numStack, opStack.pop());
                if (opStack.isEmpty()) throw new ParseException("Mismatched parentheses");
                opStack.pop(); // 彈出 '('
            } else if (PRECEDENCE.containsKey(ch)) {
                while (!opStack.isEmpty() && opStack.peek() != '(' 
                    && PRECEDENCE.get(ch) <= PRECEDENCE.get(opStack.peek())) {
                    compute(numStack, opStack.pop());
                }
                opStack.push(ch);
            }
        }
        
        // 末尾數字入棧
        if (numBuf.length() > 0) numStack.push(new BigDecimal(numBuf.toString()));
        
        // 剩餘運算符計算
        while (!opStack.isEmpty()) {
            if (opStack.peek() == '(') throw new ParseException("Mismatched parentheses");
            compute(numStack, opStack.pop());
        }
        
        if (numStack.size() != 1) throw new ParseException("Invalid expression");
        return numStack.pop();
    }

    private void compute(Deque<BigDecimal> nums, char op) {
        if (nums.size() < 2) throw new ParseException("Insufficient operands");
        BigDecimal b = nums.pop(), a = nums.pop();
        switch (op) {
            case '+': nums.push(a.add(b)); break;
            case '-': nums.push(a.subtract(b)); break;
            case '*': nums.push(a.multiply(b)); break;
            case '/': 
                if (b.compareTo(BigDecimal.ZERO) == 0) throw new ArithmeticException("Div by zero");
                nums.push(a.divide(b, 10, RoundingMode.HALF_UP)); break;
        }
    }
}

三、方案對比與選型建議

維度遞歸下降雙棧算法
代碼結構清晰對應文法、易擴展函數/變量緊湊、單函數完成
錯誤定位精確到位置、語法錯誤信息豐富較難定位具體位置
性能遞歸調用開銷、深度受棧限制迭代、無遞歸、極高性能
擴展性易添加函數、變量、賦值、位運算需重構優先級表、處理邏輯
適用場景DSL、腳本引擎、配置表達式嵌入式、高頻計算、簡單四則運算

四、生產級增強建議

  1. 詞法分析器分離:Token 流預處理,支援關鍵字、標識符、註釋
  2. AST 構建:解析生成抽象語法樹,支援優化、解釋執行、代碼生成
  3. 函數支援:sin()、max()、if() 等內建函數
  4. 變量綁定:Map<String, BigDecimal> variables 運行時賦值
  5. 精度控制:MathContext 統一精度與舍入模式
  6. 安全沙箱:限制表達式複雜度、執行時間、內存佔用

五、總結

四則運算解析器是理解編譯前端的最佳切入點。遞歸下降法結構清晰、擴展性強,適合構建 DSL 或腳本引擎;雙棧算法性能極致、代碼緊湊,適合高頻計算場景。掌握文法設計、優先級處理、錯誤恢復三大核心技能,即可舉一反三構建更複雜的表達式引擎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved