1. 引言

本文将深入探讨数学表达式中缀表示法转后缀表示法的算法原理及Java实现。后缀表达式(逆波兰表示法)在计算机科学中具有重要地位,尤其在表达式求值场景下优势明显。

2. Java中的表达式

编程语言如Java支持定义和处理多种数学表达式。表达式由变量、常量和运算符组合而成,常见类型包括算术表达式和逻辑表达式。

2.1. 算术表达式

算术表达式包含加(+)、减(-)、乘(*)、除(/)和取模(%)等运算符,与变量或常量结合产生算术结果:

int x = 100;
int y = 50;

int sum = x + y;
int prod = x * y;

int remainder = x % y;

2.2. 逻辑表达式

逻辑表达式使用逻辑运算符替代算术运算符,常见运算符包括逻辑与(AND)、或(OR)、非(NOT)和异或(XOR):

boolean andResult = (true && false); // 逻辑与
boolean orResult = (true || false);  // 逻辑或
boolean notResult = !false;          // 逻辑非

关系表达式主要用于比较逻辑,产生布尔值true/false:

int x = 10;
int y = 8;

boolean bigger = x > y; // true

3. 表达式表示法

数学表达式有不同表示方式,称为**表示法(Notation)**,根据运算符和操作数的相对位置区分。

3.1. 中缀表示法

中缀表示法中运算符位于操作数之间,是最常见的表达式形式:

int sum = (a + b) + (c * d);

⚠️ 运算符优先级会导致歧义,需使用括号明确优先级。

3.2. 前缀表示法

前缀表示法(波兰表示法)中运算符位于操作数之前:

int result = * + a b - c d;

3.3. 后缀表示法

后缀表示法(逆波兰表示法)中运算符位于操作数之后:

int result = a b + c d - *;

✅ 前缀和后缀表示法消除了运算符优先级歧义,无需括号,在表达式求值中效率更高。

4. 问题定义

给定中缀表达式作为输入,设计算法将其转换为后缀表达式(逆波兰表示法)。

示例:

输入:  (a + b) * (c - d)
输出: ab+cd-*
输入: a+b*(c^d-e)^(f+g*h)-i
输出: abcd^e-fgh*+^*+i-

输入始终是有效中缀表达式,无需额外验证。

5. 解决方案

将问题分解为多个子步骤逐步解决。

5.1. 运算符与操作数识别

输入是字符串形式的中缀表达式,实现转换前需区分运算符和操作数。操作数可以是大小写英文字母:

private boolean isOperand(char ch) {
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
}

输入包含2种括号字符和5种运算符(除操作数外)。

5.2. 优先级与结合性

定义运算符优先级并分配整数值:

  • ^(幂运算)优先级最高(3)
  • */ 优先级次之(2)
  • +- 优先级最低(1)
int getPrecedenceScore(char ch) {
    switch (ch) {
    case '^':
        return 3;

    case '*':
    case '/':
        return 2;

    case '+':
    case '-':
        return 1;
    }
    return -1;
}

相同优先级运算符的扫描顺序(结合性)通常为左到右,幂运算例外(右到左):

char associativity(char ch) {
    if (ch == '^') {
        return 'R';
    }
    return 'L';
}

6. 转换算法

中缀表达式中运算符引用其相邻操作数,而后缀表达式中运算符引用其前两个操作数。转换过程需处理嵌套括号,最内层括号优先转换,这种后进先出特性暗示使用栈数据结构。

6.1. 栈与优先级条件

使用栈跟踪运算符,需定义规则决定运算符加入后缀表达式还是保留在栈中:

  • 遇到运算符时,若栈为空则直接压栈
  • 若栈非空,比较当前运算符与栈顶运算符优先级:
    • 当前运算符优先级高于栈顶运算符时压栈
    • 优先级相等且结合性为左到右时也压栈

核心逻辑封装如下:

boolean operatorPrecedenceCondition(String infix, int i, Stack<Character> stack) {
    return getPrecedenceScore(infix.charAt(i)) < getPrecedenceScore(stack.peek())
      || getPrecedenceScore(infix.charAt(i)) == getPrecedenceScore(stack.peek())
      && associativity(infix.charAt(i)) == 'L';
}

6.2. 扫描与转换流程

完整转换算法步骤:

  1. 遍历输入字符串
  2. 操作数直接加入结果
  3. 运算符按优先级条件处理:
    • 满足条件则弹出栈顶运算符到结果
    • 当前运算符压栈
  4. 扫描结束后弹出栈中所有运算符
String infixToPostfix(String infix) {
    StringBuilder result = new StringBuilder();
    Stack<Character> stack = new Stack<>();

    for (int i = 0; i < infix.length(); i++) {
        char ch = infix.charAt(i);

        if (isOperand(ch)) {
            result.append(ch);
        } else {
            while (!stack.isEmpty() && (operatorPrecedenceCondition(infix, i, stack))) {
                result.append(stack.pop());
            }
            stack.push(ch);
        }
    }

    while (!stack.isEmpty()) {
        result.append(stack.pop());
    }

    return result.toString();
}

6.3. 算法执行示例

以表达式 a + b * c - d 为例

符号  结果    栈
a     a       []
+     a       [+]
b     ab      [+]
*     ab      [+,*]
c     abc     [+,*]
-     abc*+   [-]
d     abc*+d- []

最终后缀表达式:abc*+d-

6.4. 带括号的转换算法

中缀表达式常用括号解决优先级歧义,需调整算法处理括号:

  • 遇到 ( 直接压栈
  • 遇到 ) 弹出栈中运算符直到 (

改进后的完整实现:

String infixToPostfix(String infix) {
    StringBuilder result = new StringBuilder();
    Stack<Character> stack = new Stack<>();

    for (int i = 0; i < infix.length(); i++) {
        char ch = infix.charAt(i);

        if (isOperand(ch)) {
            result.append(ch);
        } else if (ch == '(') {
            stack.push(ch);
        } else if (ch == ')') {
            while (!stack.isEmpty() && stack.peek() != '(') {
                result.append(stack.pop());
            }
            stack.pop(); // 弹出 '('
        } else {
            while (!stack.isEmpty() && (operatorPrecedenceCondition(infix, i, stack))) {
                result.append(stack.pop());
            }
            stack.push(ch);
        }
    }

    while (!stack.isEmpty()) {
        result.append(stack.pop());
    }

    return result.toString();
}

带括号表达式 (a+b)*(c-d) 的转换过程

符号 结果    栈
(    []      [(]
a    a       [(]
+    a       [(,+]
b    ab      [(,+]
)    ab+     []
*    ab+     [*]
(    ab+     [*,(]
c    ab+c    [*,(]
-    ab+c    [*,(,-]
d    ab+cd   [*,(,-]
)    ab+cd-* []

最终结果:ab+cd-*

7. 补充思考

虽然中缀表达式更符合人类阅读习惯,但后缀表达式具有显著优势:

  • 无需括号:操作数和运算符的顺序已明确运算顺序
  • 消除歧义:彻底解决运算符优先级和结合性导致的歧义
  • 计算效率:天然适合栈结构求值,成为编程语言实现的事实标准

8. 总结

本文系统介绍了数学表达式的三种表示法(中缀、前缀、后缀),重点阐述了中缀转后缀的算法原理及Java实现。通过示例演示了算法执行过程,并展示了带括号表达式的处理方案。

本文源代码可在 GitHub 获取。


原始标题:Convert Infix to Postfix Expressions in Java | Baeldung