每日算法 — 使用java实现栈:中缀表达式转后缀与计算器求值

引言

栈(Stack)是一种后进先出(LIFO, Last In First Out)的线性数据结构。它的操作限制在表的一端进行,这一端称为栈顶。栈在计算机科学中应用广泛,从函数调用到表达式求值,从浏览器后退到撤销操作,无处不在。本文将通过中缀表达式转后缀表达式(逆波兰表示法)和计算器求值两个经典场景,深入讲解栈的原理与Java实现。

栈的基本概念

栈支持两种核心操作:
– push:将元素压入栈顶
– pop:将栈顶元素弹出

此外通常还有peek(查看栈顶但不弹出)和isEmpty(判空)等辅助操作。

栈的基础实现

/**
 * 基于数组的栈实现
 * 支持动态扩容
 */
public class ArrayStack<T> {
    private Object[] data;
    private int size;
    private static final int DEFAULT_CAPACITY = 16;

    public ArrayStack() {
        this.data = new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    /**
     * 压栈操作
     */
    public void push(T item) {
        ensureCapacity();
        data[size++] = item;
    }

    /**
     * 弹栈操作
     */
    @SuppressWarnings("unchecked")
    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        T item = (T) data[--size];
        data[size] = null; // 防止内存泄漏
        return item;
    }

    /**
     * 查看栈顶元素
     */
    @SuppressWarnings("unchecked")
    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return (T) data[size - 1];
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    private void ensureCapacity() {
        if (size == data.length) {
            data = Arrays.copyOf(data, data.length * 2);
        }
    }
}

中缀表达式转后缀表达式

中缀表达式是我们日常书写的表达式形式,如 3 + 5 * 2。后缀表达式(逆波兰表示法)将运算符放在操作数之后,如 3 5 2 * +。后缀表达式的优点是不需要括号即可明确运算顺序,且便于计算机求值。

转换算法使用栈来处理运算符:

import java.util.*;

/**
 * 表达式转换器:中缀转后缀
 */
public class InfixToPostfix {

    // 运算符优先级
    private static final Map<Character, Integer> PRECEDENCE = new HashMap<>();
    static {
        PRECEDENCE.put('+', 1);
        PRECEDENCE.put('-', 1);
        PRECEDENCE.put('*', 2);
        PRECEDENCE.put('/', 2);
        PRECEDENCE.put('^', 3); // 乘方
    }

    /**
     * 将中缀表达式转换为后缀表达式
     * @param infix 中缀表达式,支持 + - * / ^ 和括号
     * @return 后缀表达式(以空格分隔)
     */
    public static String convert(String infix) {
        StringBuilder postfix = new StringBuilder();
        ArrayStack<Character> operatorStack = new ArrayStack<>();

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

            // 跳过空格
            if (ch == ' ') continue;

            // 数字或小数点:直接输出
            if (Character.isDigit(ch) || ch == '.') {
                // 处理多位数
                while (i < infix.length() && 
                       (Character.isDigit(infix.charAt(i)) || infix.charAt(i) == '.')) {
                    postfix.append(infix.charAt(i));
                    i++;
                }
                postfix.append(' ');
                i--; // 回退一位,因为for循环会自增
            }
            // 左括号:压栈
            else if (ch == '(') {
                operatorStack.push(ch);
            }
            // 右括号:弹出到左括号
            else if (ch == ')') {
                while (!operatorStack.isEmpty() && operatorStack.peek() != '(') {
                    postfix.append(operatorStack.pop()).append(' ');
                }
                if (!operatorStack.isEmpty() && operatorStack.peek() == '(') {
                    operatorStack.pop(); // 弹出左括号
                }
            }
            // 运算符
            else if (isOperator(ch)) {
                // 处理负号(一元运算符)
                if (ch == '-' && (i == 0 || infix.charAt(i - 1) == '(' || isOperator(infix.charAt(i - 1)))) {
                    postfix.append(ch); // 将负号与数字合并
                } else {
                    // 弹出优先级大于等于当前运算符的运算符
                    while (!operatorStack.isEmpty() && 
                           operatorStack.peek() != '(' &&
                           precedence(operatorStack.peek()) >= precedence(ch)) {
                        postfix.append(operatorStack.pop()).append(' ');
                    }
                    operatorStack.push(ch);
                }
            }
        }

        // 弹出栈中剩余运算符
        while (!operatorStack.isEmpty()) {
            postfix.append(operatorStack.pop()).append(' ');
        }

        return postfix.toString().trim();
    }

    private static boolean isOperator(char ch) {
        return PRECEDENCE.containsKey(ch);
    }

    private static int precedence(char op) {
        return PRECEDENCE.getOrDefault(op, 0);
    }
}

后缀表达式求值

后缀表达式的求值同样使用栈,过程更为直接:遇到数字压栈,遇到运算符弹出两个操作数计算后将结果压栈。

/**
 * 后缀表达式求值器
 */
public class PostfixEvaluator {

    /**
     * 计算后缀表达式的值
     */
    public static double evaluate(String postfix) {
        ArrayStack<Double> valueStack = new ArrayStack<>();
        String[] tokens = postfix.split("\\s+");

        for (String token : tokens) {
            if (token.isEmpty()) continue;

            if (isNumber(token)) {
                valueStack.push(Double.parseDouble(token));
            } else if (token.length() == 1 && isOperator(token.charAt(0))) {
                // 弹出两个操作数(注意顺序:先弹出的是右操作数)
                double b = valueStack.pop();
                double a = valueStack.pop();
                double result = applyOperator(a, b, token.charAt(0));
                valueStack.push(result);
            }
        }

        return valueStack.pop();
    }

    private static boolean isNumber(String s) {
        try {
            Double.parseDouble(s);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    private static boolean isOperator(char ch) {
        return "+-*/^".indexOf(ch) >= 0;
    }

    private static double applyOperator(double a, double b, char op) {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': 
                if (b == 0) throw new ArithmeticException("除零错误");
                return a / b;
            case '^': return Math.pow(a, b);
            default: throw new IllegalArgumentException("未知运算符: " + op);
        }
    }
}

完整计算器实现

/**
 * 基于栈的完整计算器
 * 支持中缀表达式的直接求值
 */
public class StackCalculator {

    /**
     * 直接计算中缀表达式(双栈法)
     * 一个栈存操作数,一个栈存运算符
     */
    public static double calculate(String expression) {
        ArrayStack<Double> values = new ArrayStack<>();
        ArrayStack<Character> ops = new ArrayStack<>();

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

            if (ch == ' ') continue;

            // 数字
            if (Character.isDigit(ch) || ch == '.') {
                StringBuilder num = new StringBuilder();
                while (i < expression.length() && 
                       (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                values.push(Double.parseDouble(num.toString()));
                i--;
            }
            // 左括号
            else if (ch == '(') {
                ops.push(ch);
            }
            // 右括号:计算到左括号
            else if (ch == ')') {
                while (!ops.isEmpty() && ops.peek() != '(') {
                    applyTopOperator(values, ops);
                }
                ops.pop(); // 弹出左括号
            }
            // 运算符
            else if (isOperator(ch)) {
                while (!ops.isEmpty() && 
                       ops.peek() != '(' &&
                       precedence(ops.peek()) >= precedence(ch)) {
                    applyTopOperator(values, ops);
                }
                ops.push(ch);
            }
        }

        // 计算剩余运算符
        while (!ops.isEmpty()) {
            applyTopOperator(values, ops);
        }

        return values.pop();
    }

    private static void applyTopOperator(ArrayStack<Double> values, ArrayStack<Character> ops) {
        double b = values.pop();
        double a = values.pop();
        char op = ops.pop();
        values.push(apply(a, b, op));
    }

    private static double apply(double a, double b, char op) {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': 
                if (b == 0) throw new ArithmeticException("除零错误");
                return a / b;
            default: throw new IllegalArgumentException("未知运算符");
        }
    }

    private static boolean isOperator(char ch) {
        return "+-*/".indexOf(ch) >= 0;
    }

    private static int precedence(char op) {
        if (op == '+' || op == '-') return 1;
        if (op == '*' || op == '/') return 2;
        return 0;
    }
}

括号匹配检测

栈的另一个经典应用是括号匹配检测:

/**
 * 括号匹配检测器
 */
public class BracketMatcher {

    private static final Map<Character, Character> PAIRS = new HashMap<>();
    static {
        PAIRS.put(')', '(');
        PAIRS.put('}', '{');
        PAIRS.put(']', '[');
    }

    /**
     * 检测字符串中的括号是否匹配
     */
    public static boolean isValid(String s) {
        ArrayStack<Character> stack = new ArrayStack<>();

        for (char ch : s.toCharArray()) {
            if (ch == '(' || ch == '{' || ch == '[') {
                stack.push(ch);
            } else if (ch == ')' || ch == '}' || ch == ']') {
                if (stack.isEmpty()) return false;
                if (stack.pop() != PAIRS.get(ch)) return false;
            }
        }

        return stack.isEmpty();
    }
}

主程序与测试

public class StackMain {
    public static void main(String[] args) {
        // 测试中缀转后缀
        String[] testExpressions = {
            "3 + 5 * 2",
            "(3 + 5) * 2",
            "10 - 4 / 2",
            "2 ^ 3 + 4 * 5",
            "((1 + 2) * 3 - 4) / 5"
        };

        System.out.println("=== 中缀转后缀 ===");
        for (String expr : testExpressions) {
            String postfix = InfixToPostfix.convert(expr);
            double value = PostfixEvaluator.evaluate(postfix);
            System.out.printf("%s -> %s = %.2f%n", expr, postfix, value);
        }

        // 测试直接计算
        System.out.println("\n=== 直接计算 ===");
        for (String expr : testExpressions) {
            double value = StackCalculator.calculate(expr);
            System.out.printf("%s = %.2f%n", expr, value);
        }

        // 测试括号匹配
        System.out.println("\n=== 括号匹配检测 ===");
        String[] bracketTests = {"()", "()[]{}", "(]", "([)]", "{[]}", ""};
        for (String s : bracketTests) {
            System.out.printf("\"%s\" -> %s%n", s, BracketMatcher.isValid(s));
        }
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
push/pop/peek O(1) O(1)
中缀转后缀 O(n) O(n)
后缀求值 O(n) O(n)
括号匹配 O(n) O(n)

其中n为表达式长度。

总结

栈的简单LIFO特性使其成为解决”嵌套”和”反转”问题的利器。除了本文介绍的表达式处理,栈还广泛应用于函数调用栈、浏览器前进后退、撤销操作、深度优先搜索等场景。理解栈的本质,有助于在遇到相关问题时快速选择正确的数据结构。