08、数据结构和算法 - 实战:中缀表达式转逆波兰表达式

1.1 四则混合运算

加法、减法、乘法、除法,统称为四则混合运算。其中,加法和减法叫做第一级运算;乘法和除法叫做第二级运算。表达式(算式):如:1+((2+3)*4)-5=16

运算顺序

1、 同级运算时,从左到右依次计算;
2、 两级运算时,先算乘除,后算加减。
3、 有括号时,先算括号里面的,再算括号外面的;
4、 有多层括号时,先算小括号里的,再算中括号里面的,再算大括号里面的,最后算括号外面的。
5、 在混合运算中,先算括号内的数 ,括号从小到大,然后从高级到低级。

1.2 表达式分类

在计算机看待四则混合运算表达式(算式),就是个字符串,所以首先必须把它拆分成计算机可以操作的数据单元,就是Tokenize。比如【1 2 3 4 5】是操作数,【+ - * /】是操作符。但是,表达式(算式)有优先级之分,先算乘除,后算加减,也就是算式这种人类描述数学表达式(算式)的语言,有其自身的语法,所以对于一个表达式(算式),我们还得对这个表达式(算式)再做分析。

中缀表达式

中缀表达式是常见的运算表达式,这种就是中缀表达式,也就是运算符在运算数的中间。如 1+((2+3)*4)-5=16。中缀表达式在我们日常生活中应用最为广泛,也最符合人的计算思维。

前缀表达式

波兰科学家扬·武卡谢维奇(Jan ukasiewicz)发明了一种不需要括号的计算表达式的表示法将操作符号写在操作数之前,也就是前缀表达式,即波兰式(Polish Notation, PN)。前缀表达式就是不含括号的算术表达式,而且它是将运算符写在前面,操作数写在后面的表达式,也称为“波兰式”。例如,+ 1 - 5 * + 2 3 4,它等价于1+((2+3)*4)-5 。

中缀表达式:1+((2+3)*4)-5

1、 操作符串提取到前面就是:+1-5(*(+23)4);
2、 把括号去掉就是+1-5*+234;

后缀表达式

后缀表达式又叫逆波兰表达式。逆波兰表示法是波兰逻辑学家J・卢卡西维兹(J・ Lukasewicz)于1929年首先提出的一种表达式的表示方法。后来,人们就把用这种表示法写出的表达式称作“逆波兰表达式”。逆波兰表达式把运算量写在前面,把算符写在后面。例如,+ 1 - 5 * + 2 3 4,它等价于1+((2+3)*4)-5 。

后缀表达式:1+((2+3)*4)-5

1、 操作符串提取到前面就是:1((23+)4*)+5-;
2、 把括号去掉就是123+4*+5-;

逆波兰表达式的优势:在于只用两种简单操作,入栈和出栈就可以搞定任何普通表达式的运算。其运算方式如下:

如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。

1.2 中缀表达式转逆波兰表达式

思路

1、 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
2、 从左至右扫描中缀表达式;
3、 遇到操作数时,将其压s2;
4、 遇到运算符时,比较其与s1栈顶运算符的优先级:;

  • 4.1 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈。
  • 4.2 否则,若优先级小于或等于栈顶运算符优先级,将s1栈顶的运算符弹出并压入到s2中。将当前运算符压入栈中,
  • 4.3 再次转到(4.1)与s1中新的栈顶运算符相比较。

5、 遇到括号时:;

  • 5.1 如果是左括号“(”,则直接压入s1。
  • 5.2 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃;

6、 重复步骤2至5,直到表达式的最右边;
7、 将s1中剩余的运算符依次弹出并压入s2;
8、 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式(转换为前缀表达式时不用逆序)【s2使用ArrayList容器,利用ArrayList后进先出特点,直接遍历结果即可,无须逆序】;

 

扫描到的元素 s2(栈底->栈顶) s1 (栈底->栈顶) 说明
1 1 数字,直接入栈
+ 1 + s1为空,运算符直接入栈
( 1 + ( 左括号,直接入栈
( 1 + ( ( 同上
2 1 2 + ( ( 数字
+ 1 2 + ( ( + s1栈顶为左括号,运算符直接入栈
3 1 2 3 + ( ( + 数字
) 1 2 3 + + ( 右括号,弹出运算符直至遇到左括号
× 1 2 3 + + ( × s1栈顶为左括号,运算符直接入栈
4 1 2 3 + 4 + ( × 数字
) 1 2 3 + 4 × + 右括号,弹出运算符直至遇到左括号
- 1 2 3 + 4 × + - -与+优先级相同,因此弹出+,再压入-
5 1 2 3 + 4 × + 5 - 数字
到达最右端 1 2 3 + 4 × + 5 - s1中剩余的运算符

1.3 JAVA实现中缀表达式转逆波兰表达式

后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。

package com.yuanxw.datastructure.chapter8;

import com.yuanxw.datastructure.chapter7.CustomArrayStack;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * 算式表达式转逆波兰表达式
 */
public class ArithmeticReversePolishExpression {
   
     
    public static void main(String[] args) {
   
     
        String expression = "1+(2+3)*4-5";
        StringBuilder builder = new StringBuilder();
        List<String> polishExpressionList = toReversePolishExpressionList(toList(expression));
        polishExpressionList.forEach(element -> builder.append(element));

        System.out.println(String.format("算式表达式:【%s】", expression));
        System.out.println(String.format("转逆波兰表达式:【%s】", builder.toString()));
        System.out.println(String.format("转逆波兰表达式计算结果:%s=%s", expression, calculate(polishExpressionList)));
    }

    /**
     * 转List结构数据
     *
     * @param expression
     * @return
     */
    public static List<String> toList(String expression) {
   
     
        if (expression == null || expression.length() <= 0) {
   
     
            throw new NullPointerException("expression表达式不能为空!");
        }

        // 去掉空格、回车、换行符、制表符
        expression = expression.replaceAll("\\s*|\t|\r|\n", "");
        List<String> expressionList = new ArrayList<>();
        char[] chars = expression.toCharArray();
        for (int i = 0; i < chars.length; i++) {
   
     
            // 判断是否是数字
            if (!Character.isDigit(chars[i])) {
   
     
                expressionList.add(String.valueOf(chars[i]));
            } else {
   
     
                // 多位数字处理
                String number = "";
                for (int j = i; j < chars.length && chars[j] >= 48 && chars[j] <= 57; j++) {
   
     
                    number += chars[j];
                }
                expressionList.add(number);
            }
        }
        // 转list数据
        return expressionList;
    }

    /**
     * 转逆波兰表达式
     *
     * @param expressionList
     * @return
     */
    public static List<String> toReversePolishExpressionList(List<String> expressionList) {
   
     
        //  1. 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
        CustomArrayStack<String> s1 = new CustomArrayStack();
        List<String> s2 = new ArrayList<>();

        // 2. 从左至右扫描中缀表达式;
        for (String item : expressionList) {
   
     
            // 3. 遇到操作数时,将其压s2;
            if (Pattern.matches("-?[0-9]+(\\.[0-9]+)?", item)) {
   
     
                s2.add(item);
            }

            /**
             * 4. 遇到运算符时,比较其与s1栈顶运算符的优先级:
             * 4.2 否则,若优先级小于或等于栈顶运算符优先级,将s1栈顶的运算符弹出并压入到s2中。将当前运算符压入栈中,
             * 4.3 再次转到(4.1)与s1中新的栈顶运算符相比较;
             */
            if (isSymbol(item)) {
   
     
                while (s1.size() > 0 && isSymbol(s1.peek()) && priority(item) <= priority(s1.peek())) {
   
     
                    s2.add(s1.pop());
                }
                s1.push(item);
            }

            // 5. 遇到括号时:
            // 5.1 如果是左括号“(”,则直接压入s1;
            if (item.equals("(")) {
   
     
                s1.push(item);
            }

            // 5.2 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃;
            if (item.equals(")")) {
   
     
                while (!s1.peek().equals("(")) {
   
     
                    s2.add(s1.pop());
                }
                // 将左边的括号,弹栈
                s1.pop();
            }
        }
        while (s1.size() > 0) {
   
     
            s2.add(s1.pop());
        }
        return s2;
    }

    /**
     * 计算器
     *
     * @param expressionList
     * @return
     */
    public static BigDecimal calculate(List<String> expressionList) {
   
     
        if (expressionList == null || expressionList.size() <= 1) {
   
     
            throw new NullPointerException("逆波兰表达式列表不能为空!");
        }

        CustomArrayStack<BigDecimal> customArrayStack = new CustomArrayStack();
        for (String item : expressionList) {
   
     
            if (isNumber(item)) {
   
     
                customArrayStack.push(new BigDecimal(item));
            } else {
   
     
                BigDecimal number1 = customArrayStack.pop();
                BigDecimal number2 = customArrayStack.pop();
                BigDecimal result = operation(number1, number2, item);
                customArrayStack.push(result);
            }
        }
        return customArrayStack.pop();
    }

    /**
     * 是否为数字
     *
     * @param str
     * @return
     */
    public static boolean isNumber(String str) {
   
     
        return Pattern.matches("-?[0-9]+(\\.[0-9]+)?", str);
    }

    /**
     * 是否是运算符
     *
     * @param str
     * @return
     */
    public static boolean isSymbol(String str) {
   
     
        return "+-*/".contains(str);
    }

    /**
     * 返回运算符的优先级
     * 数字越大,则优先级就越高.
     *
     * @param value
     * @return
     */
    public static int priority(String value) {
   
     
        if ("+-".contains(value)) {
   
     
            return 1;
        } else if ("*/".contains(value)) {
   
     
            return 2;
        } else {
   
     
            throw new RuntimeException("暂不支持操作符:" + value);
        }
    }

    /**
     * 计算操作
     *
     * @param number1
     * @param number2
     * @param symbol
     * @return
     */
    public static BigDecimal operation(BigDecimal number1, BigDecimal number2, String symbol) {
   
     
        switch (symbol) {
   
     
            case "+":
                return number2.add(number1);
            case "-":
                return number2.subtract(number1);
            case "*":
                return number2.multiply(number1); //
            case "/":
                return number2.divide(number1);
            default:
                throw new RuntimeException("暂不支持操作符:" + symbol);

        }
    }
}

执行结果:

算式表达式:【1+(2+3)*4-5】
转逆波兰表达式:【123+4*+5-】
转逆波兰表达式计算结果:1+(2+3)*4-5=16