前缀、中缀、后缀表达式及简易运算实现总结


title: 前缀、中缀、后缀表达式及简易运算实现总结
date: 2023-06-30 10:25:50
tags:

  • 表达式
    categories:
  • 开发知识及其他
    cover: https://cover.png
    feature: false

1. 概念

1.1 什么是前缀、中缀、后缀表达式?

  • 前缀表达式:又称波兰式Polish Notation),操作符以前缀形式位于两个运算数前(如:3 + 2 的前缀表达形式就是 + 3 2)
  • 中缀表达式:操作符以中缀形式位于运算数中间(如:3 + 2),是我们日常通用的算术和逻辑公式表示方法
  • 后缀表达式:又称逆波兰式Reverse Polish Notation - RPN),操作符以后缀形式位于两个运算数后(如:3 + 2 的后缀表达形式就是 3 2 +)

中缀表达式往往需要使用括号将操作符和对应的操作数括起来,用于指示运算的次序,如 5 * (2 + 1) 虽然 * 的优先级高于 + ,但括号的存在表示应优先执行括号内的 + 运算。适合于人类的思维结构和运算习惯,但并不适用于计算机

与中缀表达式不同,前缀和后缀表达式都不需要使用括号来标识操作符的优先级,适用于计算机。不过后缀表达式的计算按操作符从左到右出现的顺序依次执行(不考虑运算符之间的优先级),更加符合人类的阅读习惯,因此实际计算机程序中,基本都是用后缀表达式来存储公式的,前缀表达式效果次之。对于中缀表达式,我们则可以先将其转为后缀表达式,再进行求值

1.2 树结构对应

其实前缀表达式、中缀表达式、后缀表达式就是通过树来存储和计算表达式的三种不同方式,分别对应树的先序遍历、中序遍历、后序遍历。如下图,这是一颗二叉树

前缀、中缀、后缀表达式及简易运算实现总结_第1张图片

  • 上面的树,先序遍历就是 *+a−bcd,即对应前缀表达式
  • 中序遍历是 a+b−c∗d,但是这样的表示是有歧义的,这样表示 ab 是一颗子树,cd 是一颗子树,然后相减,所以中缀表达式必须借助括号,才能正确地表达出想要的结果。中缀表达式为:(a+(b−c))∗d,括号表示一个子树的整体
  • 后序遍历是 abc−+d∗,即对应的后缀表达式

2. 表达式求值

2.1 通过树结构存储和求值表达式

实现思路比较简单,如果节点上存的是参数,那么该参数的值,就是该节点的值;如果节点上存的操作符,拿该节点左子树和右子树做对应运算,得到的结果作为该节点的值

代码略

2.2 前缀表达式解析和求值

∗ + a − b c d ∗+a−bcd +abcd

观察前缀表达式的规律可以发现,每当连续出现两个数值时,前面必定会有一个操作符,这是先序遍历的特征决定的(根左右,根即为表达式),因此我们依次取三个元素出来,判断符合连续两个数值条件的进行运算,就可以得到一个操作符节点的数值,如此反复递归,最终就能求出表达式的值

代码略

2.3 后缀表达式解析和求值

a b c − + d ∗ abc−+d∗ abc+d

和前缀表达式类似,其实也就是后序遍历的特征,即只要有运算符出现的地方,前面两个元素一定是操作数(左右根),然后同样取三个元素出来,判断符合条件的进行运算

详细代码见 3

2.4 中缀表达式转后缀表达式

( a + ( b − c ) ) ∗ d (a+(b−c))∗d (a+(bc))d

中缀表达式直接求值比较麻烦,所以我们将其转换为后缀表达式,再求值就方便了。中缀表达式转后缀表达式的难点在于,要考虑括号和运算符优先级,步骤如下,这个转换算法不是凭空产生的,而是根据后缀表达式的特点反推出来的

  1. 创建两个栈,S1 用来存输出元素,S2 用来存运算符。由于表达式中的运算符是有优先级的,所以必须通过栈来暂存起来
  2. 从中缀表达式栈顶开始,向栈尾逐个读取元素
  3. 如果读到操作数,直接加到 S1 栈尾。因为后缀表达式操作数永远是在运算符前面的
  4. 如果读到左括号,则直接压入 S2 栈顶。因为左括号要等到右括号时才能处理
  5. 如果读到运算符,且 S2 栈为空或 S2 栈顶元素为左括号,则直接压入 S2 栈顶。因为这种情况不需要比较运算符优先级
  6. 如果读到运算符,且 S2 栈顶也为运算符,且当前运算符优先级大于栈顶元素,则将当前运算符压入 S2 栈顶。因为后面读取到的运算符可能比当前运算符优先级更高,因此暂时不能输出当前运算符
  7. 如果读到运算符,且 S2 栈顶也为运算符,且当前运算符优先级小于等于栈顶元素,则将 S2 栈顶运算符弹出,加到 S1 栈尾。因为优先级高的运算符要先参加运算。注意,这是一个递归过程,因为 S2 中可能已存在多个运算符,它们的优先级可能都大于等于当前运算符,当这些运算符都弹出时,再将当前运算符压入 S2 栈顶
  8. 如果读到右括号,则将 S2 内首个左括号以上的运算符,全部加到 S1 栈尾。因为括号的优先级是最高的,立刻进行运算

例:中缀表达式 2*(3+5)+7/1-4 转换为后缀表达式

可以先转换为树,然后后序遍历得到后缀表达式,再和通过上面步骤推算出来的结果进行验证,判断是否正确。转换需要强调的是,我们用括号表示优先计算

表达式 2*(3+5)+7/1-4 中我们约定 * 和 / 的优先级高于 + 和 -,因此 + 和 - 要优先计算时需要加上括号。但是本身对于 + 和 - 来说,* 和 / 优先级高也是一种优先计算,优先计算就需要加上括号,只是我们一开始约定了先算 * 和 /,同时也为了方便,因此省略了括号

包括同级的 * 和 / 或 + 和 -,我们约定了从左往右算,其实先算左边的,也是一种优先计算,我们给优先计算的都加上括号,那么原式应为:((2*(3+5))+(7/1)) -4

强调这一点主要为了转换成树的时候方便划分左右子树,括号为一个子树的整体,这样一来转换成树的结构就很清晰了,[左子树 运算符 右子树]

前缀、中缀、后缀表达式及简易运算实现总结_第2张图片

后序遍历为:235+*71/+4-,即后缀表达式

此时再通过上面的步骤得到后缀表达式

前缀、中缀、后缀表达式及简易运算实现总结_第3张图片

可以看到最终结果也是 235+*71/+4-

详细代码见 3

3. 简易运算实现

Calculator 类

public class Calculator {

    private static final Map<String, Integer> OPERATORS = MapUtil.builder("+", 1).put("-", 1).put("*", 2).put("/", 2)
            .put("%", 2).put("^", 3).put("(", 0).put(")", 0).build();

    private Calculator() {
    }

    public static double calculate(String equation) {
        if (!BaseUtil.isWholeSymbol(equation)) {
            throw new IllegalArgumentException("请确认括号是否完整");
        }

        Deque<String> operand = new ArrayDeque<>();
        Deque<String> operator = new ArrayDeque<>();

        for (String str : toList(equation)) {
            if (NumberUtils.isCreatable(str)) {
                operand.push(str);
                continue;
            }

            Integer opt = OPERATORS.get(str);
            if (null == opt) {
                throw new IllegalArgumentException("操作符不合法");
            }

            if (StrPool.LBRACKET.value().equals(str) || operator.isEmpty() || opt > OPERATORS.get(operator.peek())) {
                operator.push(str);
            } else if (StrPool.RBRACKET.value().equals(str)) {
                // 判断是否是右括号, 存在右括号则运算符栈必有左括号, 即运算符栈不为空
                while (!operator.isEmpty()) {
                    if (StrPool.LBRACKET.value().equals(operator.peek())) {
                        operator.pop();
                        break;
                    } else {
                        String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
                        operand.push(calculate);
                    }
                }
            } else if (opt <= OPERATORS.get(operator.peek())) {
                while (!operator.isEmpty() && opt <= OPERATORS.get(operator.peek())) {
                    String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
                    operand.push(calculate);
                }
                operator.push(str);
            }
        }

        while (!operator.isEmpty()) {
            String calculate = calculate(operator.pop(), operand.pop(), operand.pop());
            operand.push(calculate);
        }
        return Double.parseDouble(operand.pop());
    }


    public static List<String> toList(String str) {
        List<String> list = new ArrayList<>();
        StringBuilder builder = new StringBuilder();

        String replace = str.replaceAll("\\s*", "");
        char[] chars = replace.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            boolean isMinus = '-' == chars[i] && (i == 0 || '(' == chars[i - 1]);
            if (isMinus) {
                builder.append(chars[i]);
                continue;
            }

            String val = String.valueOf(chars[i]);
            if (null != OPERATORS.get(val)) {
                if (StringUtil.INSTANCE.isNotBlank(builder)) {
                    list.add(builder.toString());
                }
                list.add(val);
                builder = new StringBuilder();
            } else {
                builder.append(chars[i]);
            }
        }

        if (StringUtil.INSTANCE.isNotBlank(builder)) {
            list.add(builder.toString());
        }
        return list;
    }

    private static String calculate(String operator, String val2, String val1) {
        double pre = Double.parseDouble(val1);
        double suf = Double.parseDouble(val2);

        switch (operator) {
            case "+":
                return pre + suf + "";
            case "-":
                return pre - suf + "";
            case "*":
                return pre * suf + "";
            case "/":
                return pre / suf + "";
            case "%":
                return pre % suf + "";
            case "^":
                return Math.pow(pre, suf) + "";
            default:
                return "0";
        }
    }
}

BaseUtil 类

public class BaseUtil {

    private static final Map<Character, Character> R_SYMBOL = MapUtil.builder(')', '(').put(']', '[').put('}', '{').build();

    private static final List<Character> L_SYMBOL = ListUtil.list('(', '[', '{');

    private BaseUtil() {
    }

    public static boolean isWholeSymbol(String str) {
        Deque<Character> symbol = new ArrayDeque<>();

        for (char ch : str.toCharArray()) {
            if (R_SYMBOL.containsKey(ch)) {
                if (symbol.isEmpty() || !symbol.peek().equals(R_SYMBOL.get(ch))) {
                    return false;
                }
                symbol.pop();
            } else if (L_SYMBOL.contains(ch)) {
                symbol.push(ch);
            }
        }

        return symbol.isEmpty();
    }
}

你可能感兴趣的:(开发知识及其他,服务器,数据库,运维)