Java 实例 – 利用堆栈将中缀表达式转换成后缀表达式(建议收藏)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言:为什么需要将中缀表达式转换为后缀表达式?

在编程和数学计算领域,表达式的解析是一个核心问题。日常使用的中缀表达式(如 3 + 4 * 2)虽然直观易读,但计算机在计算时需要将其转换为更高效的后缀表达式(如 3 4 2 * +)。这种转换的核心工具是堆栈(Stack),它通过“先进后出”的特性,完美解决了操作符优先级和括号嵌套的问题。本文将通过一个 Java 实例,深入讲解如何利用堆栈将中缀表达式转换为后缀表达式,并提供可运行的代码示例,帮助读者掌握这一经典算法。


基础知识:中缀表达式与后缀表达式

什么是中缀表达式?

中缀表达式是人类日常使用的表达式形式,例如 5 + 3(a + b) * c。它的特点是操作符位于两个操作数之间,但解析时需要处理复杂的优先级和括号嵌套问题。

什么是后缀表达式?

后缀表达式(Reverse Polish Notation, RPN)将操作符放在操作数之后,例如 5 3 +a b + c *。这种形式的优点是无需考虑优先级,只需按顺序计算即可,非常适合计算机高效处理。

转换的必要性

计算机在解析中缀表达式时需要额外的逻辑来判断优先级和括号,而解析后缀表达式只需一个简单的循环。因此,将中缀表达式转换为后缀表达式是许多编译器和计算器的核心步骤。


算法核心思想:堆栈的“分拣”作用

堆栈的比喻

堆栈可以想象成一个“盘子堆叠机”:每次放入新盘子时,它会压在最上面;取出时,只能取走最上面的盘子。在表达式转换中,堆栈用于暂存操作符,根据优先级决定何时将它们弹出。

转换的三大规则

  1. 操作数直接输出:遇到数字或变量时,直接追加到结果列表。
  2. 操作符处理:根据优先级和堆栈顶操作符的优先级,决定是弹出堆栈中的操作符,还是将当前操作符压入堆栈。
  3. 括号处理:左括号直接压入堆栈;右括号触发弹出操作符,直到遇到左括号为止。

算法步骤详解:以 3 + 4 * 2 为例

步骤 1:初始化堆栈和结果列表

  • 堆栈 stack 为空。
  • 结果列表 output 为空。

步骤 2:逐个处理字符

输入字符:3

  • 是操作数 → 直接添加到 outputoutput = ["3"]

输入字符:+

  • 是操作符 → 比较当前操作符 + 与堆栈顶(空)的优先级。
  • 堆栈为空 → 直接压入 +stack = ["+"]

输入字符:4

  • 是操作数 → 添加到 outputoutput = ["3", "4"]

输入字符:*

  • 是操作符 → 当前堆栈顶是 +,优先级低于 * → 将 * 压入堆栈 → stack = ["+", "*"]

输入字符:2

  • 是操作数 → 添加到 outputoutput = ["3", "4", "2"]

步骤 3:处理完所有字符后清空堆栈

  • 弹出堆栈中的所有操作符 → output = ["3", "4", "2", "*", "+"]

最终后缀表达式为:3 4 2 * +


操作符优先级表

在代码实现中,需定义操作符的优先级。以下为常见操作符的优先级(数值越大,优先级越高):

操作符优先级
(0
)0
+1
-1
*2
/2

Java 实现代码详解

类结构设计

import java.util.*;

public class InfixToPostfix {
    private static final Map<Character, Integer> precedence = new HashMap<>();
    static {
        precedence.put('(', 0);
        precedence.put('+', 1);
        precedence.put('-', 1);
        precedence.put('*', 2);
        precedence.put('/', 2);
    }

    public String convert(String infix) {
        Stack<Character> stack = new Stack<>();
        StringBuilder output = new StringBuilder();
        for (char c : infix.toCharArray()) {
            if (Character.isDigit(c)) {
                output.append(c);
            } else if (c == '(') {
                stack.push(c);
            } else if (c == ')') {
                while (!stack.isEmpty() && stack.peek() != '(') {
                    output.append(stack.pop());
                }
                stack.pop(); // 移除左括号 '('
            } else {
                while (!stack.isEmpty() && precedence.get(c) <= precedence.get(stack.peek())) {
                    if (stack.peek() == '(') break; // 遇到左括号停止弹出
                    output.append(stack.pop());
                }
                stack.push(c);
            }
            if (output.length() > 0 && !Character.isDigit(output.charAt(output.length()-1))) {
                output.append(' '); // 添加空格分隔符
            }
        }
        while (!stack.isEmpty()) {
            output.append(stack.pop());
            if (output.length() > 0 && !Character.isDigit(output.charAt(output.length()-1))) {
                output.append(' ');
            }
        }
        return output.toString().trim();
    }
}

代码关键点解析

  1. 操作符优先级表:通过 Map<Character, Integer> 存储优先级,0 表示括号无优先级。
  2. 括号处理:遇到右括号时,循环弹出堆栈元素直到左括号,并丢弃左括号。
  3. 操作符压栈逻辑:只有当前操作符优先级高于堆栈顶时才压栈,否则弹出堆栈顶元素。
  4. 空格分隔符:在操作符前后添加空格,使后缀表达式更易读(如 3 4 +)。

测试案例与调试技巧

测试案例 1:基本表达式

public static void main(String[] args) {
    InfixToPostfix converter = new InfixToPostfix();
    String infix = "3 + 4 * 2";
    System.out.println(converter.convert(infix)); // 输出:3 4 2 * +
}

测试案例 2:含括号的复杂表达式

String infix = "( 5 + 3 ) * 2 - 7";
System.out.println(converter.convert(infix)); // 输出:5 3 + 2 * 7 -

常见问题与调试方法

  1. 堆栈溢出或死循环:检查括号是否匹配,确保每对 () 都正确闭合。
  2. 操作符顺序错误:在代码中添加 System.out.println 打印堆栈状态,观察操作符压栈和弹出的时机。
  3. 数字与操作符粘连:确保输入表达式中的操作符前后有空格,或在代码中增加分隔符处理逻辑。

扩展与优化方向

1. 支持变量和函数调用

可扩展代码以识别变量名(如 a + b)或函数(如 sin(x) + 2),需在 isDigit 判断中加入字母检测。

2. 错误处理

添加对无效字符(如 $)、括号不匹配或空表达式的检查,抛出 IllegalArgumentException

3. 性能优化

  • 使用 Deque 替代 Stack 以获得更好的性能。
  • precedenceMap 替换为 switch 语句,减少哈希计算开销。

结论:从理论到实践的跨越

通过本文的讲解和代码示例,我们掌握了堆栈在表达式转换中的核心作用。这一算法不仅是编译原理的基础,也常用于构建简易计算器或表达式求值工具。建议读者尝试以下实践:

  1. 将代码扩展为支持浮点数运算。
  2. 实现后缀表达式求值功能。
  3. 用其他语言(如 Python)重写该算法,对比不同语言的堆栈实现差异。

掌握这一技能后,你不仅能解决技术面试中的经典问题,还能为更复杂的编程项目(如数学库开发)打下坚实基础。编程的魅力,正在于将看似复杂的逻辑,通过巧妙的数据结构与算法,转化为简洁优雅的代码。

最新发布