数据结构篇——栈扩展(计算器,前中后缀表达式)

上一篇博客我们讲了队列的扩展应用,本篇我们再来分析一下栈的一些应用。

计算器

说到栈的应用,最经典的就是计算器的实现了。对于给定的一个算数表达式,我们需要输出它的最终结果。
例如:对于输入(10+2-5)*(30/6) 我们需要输出35

分析过程

  1. 对于一个计算表达式,我们需要考虑运算符的优先级,还要考虑括号。按照我们人类的计算思路,会把要先运算的算好再与其他继续运算。
    例如:
  1+2*(4-3)
=>1+2*1
=>1+2
=>3
  1. 一个表达式中有两种数据:数字,运算符号(括号也算运算符号)。我们需要考虑的是使用一个栈还是使用两个栈?如果使用一个栈,当往栈里面放数字我们需要做什么处理?放运算符号呢?
  2. 我们仔细观察表达式的话,会发现一个运算符加上左右两侧的数字能够构成一个独立的表达式(不考虑括号的场景);如果有括号的话,左括号和右括号中间也是一个独立的表达式;
  3. 表达式经过运算就是一个数字;
  4. 使用栈计算表达式必然需要两个过程:入栈,出栈。对于出栈,我们一定希望先取出来的是优先级比较高的表达式,后取出来的是优先级比较低的表达式。所以就需要我们在表达式入栈的时候进行一波整理。把所有的运算逻辑按运算优先级调整入栈顺序。加法和减法相对于乘法除法必定位于栈底部,括号运算由于过于复杂,在入栈的时候我们就把括号里面的表达式变成一个数字

双栈实现

/**
 * 用两个栈实现的计算器
 */
public class StackCalculator {

    /**
     * 表达式
     */
    private String expr;

    /**
     * 操作数栈
     */
    private Stack<Number> numStack;

    /**
     * 操作符栈
     */
    private Stack<Character> operStack;

    public StackCalculator(String expr) {
        //去除表达式中的空格
        this.expr = expr.replace(" ", "");
        numStack = new Stack<>();
        operStack = new Stack<>();
    }

    /**
     * 计算表达式的值
     */
    public Number calculate() {
        char[] chars = expr.toCharArray();

        //入栈
        for (int i = 0; i < chars.length; i++) {
            //如果是数字则直接入栈
            if (Character.isDigit(chars[i])) {
                String numString = String.valueOf(chars[i]);
                //此处考虑多位数和小数的情况 如果下一位还是数字或者小数点则继续向下取值
                while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
                    i++;
                    numString += chars[i];
                }
                numStack.push(new BigDecimal(numString));
                continue;
            }

            //如果是运算符号 我们就需要在入栈的时候检查优先级顺序
            //1、如果operStack里面没有数据则直接入栈
            //2、如果operStack里面有数据,则需要比较栈顶运算符与本次运算符的优先级
            //2.1、如果本次优先级比较大,则放入栈
            //2.2、如果栈里面的优先级比较大,则从operStack取出栈顶运算符,从numStack取出两个数字进行运算,再把运算结果放回numStack
            //3、如果本次运算符是左括号,则放入栈
            //4、如果本次运算符是右括号,则不停地从operStack弹出运算符直到弹出左括号,把这两个括号中间的表达式算出结果,放入numStack
            if (operStack.size() == 0) {
                operStack.push(chars[i]);
                continue;
            }

            if (chars[i] == '(') {
                operStack.push(chars[i]);
                continue;
            }
            if (chars[i] == ')') {
                while (operStack.top() != '(') {
                    calResult();
                }
                operStack.pop();
                continue;
            }
            if (compareOper(chars[i], operStack.top())) {
                operStack.push(chars[i]);
            } else {
                calResult();
                operStack.push(chars[i]);
            }
        }

        //出栈得出最后计算结果

        while (operStack.size() != 0) {
            calResult();
        }
        return numStack.pop();
    }

    /**
     * 这个方法比较重要,巧妙的设计能够简便我们的运算
     * 当oper1和oper2的优先级一样的时候返回false,这样子就能保证在不考虑括号的情况下 operStack里面的数据量不超过2个
     * 

* 1、stackOper是乘除法 返回false * 2、putOper和stackOper同为加减法 返回false * 3、stackOper是左括号 返回true * 4、其他情况(putOper是乘除法 stackChar是加减法) 返回true */ private boolean compareOper(char putOper, char stackOper) { if (stackOper == '*' || stackOper == '/') { return false; } if ((putOper == '+' || putOper == '-') && (stackOper == '+' || stackOper == '-')) { return false; } if (stackOper == '(') { return true; } return true; } /** * 计算一次值 operStack取出一个值 numStack取出两个值 算好以后放进numStack */ private void calResult() { Character oper = operStack.pop(); //要注意第二次取出来的是 第一个操作数 Number num2 = numStack.pop(); Number num1 = numStack.pop(); BigDecimal result = null; if (oper == '+') { result = new BigDecimal(String.valueOf(num1)).add(new BigDecimal(String.valueOf(num2))); } else if (oper == '-') { result = new BigDecimal(String.valueOf(num1)).subtract(new BigDecimal(String.valueOf(num2))); } else if (oper == '*') { result = new BigDecimal(String.valueOf(num1)).multiply(new BigDecimal(String.valueOf(num2))); } else if (oper == '/') { result = new BigDecimal(String.valueOf(num1)).divide(new BigDecimal(String.valueOf(num2)), BigDecimal.ROUND_HALF_UP); } numStack.push(result); } public static void main(String[] args) { StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)"); System.out.println(stackCalculator.calculate()); } }

这里在原先的Stack中增加了一个top()方法

    /**
     * 查看栈顶的值 不取出
     */
    public T top() {
        //如果栈空了则返回null
        if (isEmpty()) {
            System.out.println("栈空了");
            return null;
        }
        return first.item;
    }

具体的实现细节,都很详细的写在了注释中。上述代码是使用了双栈,分别存放操作数和操作符。如果使用一个栈,当然也可以。每次计算的时候固定从stack中取三个元素。

前缀,中缀,后缀表达式

这几种表达式的不同之处仅仅在于操作数与操作符的相对位置不同。前中后是针对操作符而言的。以一个简单的数学表达式1+2*3举例
前缀表达式:+ 1 * 2 3
中缀表达式:1 + 2 * 3
后缀表达式:1 2 3 * +

表达式之间的相互转换思路:
1、如果大家能够很好的理解上面计算器的实现方式,对于表达式的转换应该也很容易就能想到;
2、利用栈FIFO的特性,一开始在表达式入栈的时候按照一定规律整理好。把不按照规律的表达式,在入栈时直接计算出一个临时结果放入栈中,最后按次序出栈得到最后的结果。

中缀转前缀

    /**
     * 在原来的计算器类中新加入操作符栈
     */
    private Stack<Character> operStack;
    
 	/**
     * 中缀表达式转前缀表达式
     */
    public String infix2Prefix() {
        char[] chars = expr.toCharArray();
        //入栈
        for (int i = 0; i < chars.length; i++) {
            //如果是数字则直接入栈
            if (Character.isDigit(chars[i])) {
                String numString = String.valueOf(chars[i]);
                //此处考虑多位数和小数的情况 如果下一位还是数字或者小数点则继续向下取值
                while (Character.isDigit(chars[i + 1]) || chars[i + 1] == '.') {
                    i++;
                    numString += chars[i];
                }
                exprStack.push(numString);
                continue;
            }
            //如果是运算符号 我们就需要在入栈的时候检查优先级顺序
            //1、如果operStack里面没有数据则直接入栈
            //2、如果operStack里面有数据,则需要比较栈顶运算符与本次运算符的优先级
            //2.1、如果本次优先级比较大,则放入栈
            //2.2、如果栈里面的优先级比较大,则从operStack取出栈顶运算符,从exprStack取出两个表达式进行拼接,再把运算结果放回exprStack
            //3、如果本次运算符是左括号,则放入栈
            //4、如果本次运算符是右括号,则不停地从operStack弹出运算符直到弹出左括号,把这两个括号中间的表达式算出结果,放入exprStack
            if (operStack.size() == 0) {
                operStack.push(chars[i]);
                continue;
            }

            if (chars[i] == '(') {
                operStack.push(chars[i]);
                continue;
            }
            if (chars[i] == ')') {
                while (operStack.top() != '(') {
                    getPrefix();
                }
                operStack.pop();
                continue;
            }
            if (compareOper(chars[i], operStack.top())) {
                operStack.push(chars[i]);
            } else {
                getPrefix();
                operStack.push(chars[i]);
            }
        }

        //出栈得出最后计算结果

        while (operStack.size() != 0) {
            getPrefix();
        }
        return exprStack.pop();
    }

    private void getPrefix() {
        String expr2 = exprStack.pop();
        String expr1 = exprStack.pop();
        Character oper = operStack.pop();
        //前缀表达式 符号在前
        String tempResult = oper + " " + expr1 + " " + expr2;
        exprStack.push(tempResult);
    }

    public static void main(String[] args) {
        StackCalculator stackCalculator = new StackCalculator("(10.5+2.5)*(30/6+12.5/2.5*2)");
//        System.out.println(stackCalculator.calculate());
        System.out.println(stackCalculator.infix2Prefix());
    }

我们可以看到其实总的流程跟计算器是一模一样的,修改的点只是把原来的calResult方法改为getPrefix。相信大家应该也很容易理解。

中缀转后缀

中缀转后缀的思路与中缀转前缀的思路一样,只是需要把getPrefix的实现改为getSuffix。

    private void getSuffix() {
        String expr2 = exprStack.pop();
        String expr1 = exprStack.pop();
        Character oper = operStack.pop();
        //后缀表达式 符号在前
        String tempResult = expr1 + " " + expr2 + " " + oper;
        exprStack.push(tempResult);
    }

前缀转后缀

这个留给大家自己实现吧~
其实百分之95的代码跟中缀转后缀一样。只不过需要在过程加一点判断,防止一些运行时异常发生,如数组越界异常。然后对于小数和多位数的处理也略有不同。

总结

为什么我们在实现计算器的时候要使用栈,而不是队列?
对于3个值A,B,C。设入栈(队列)的顺序为ABC,现在A已经放入了栈(队列),正要把B也按照一定逻辑放入栈(队列)中,现在需要特定来源的数据来给我们的逻辑提供支撑。
如果使用队列,对于B的处理逻辑,我们需要依靠C,也就是说我们需要依靠未来的值来处理现在的逻辑
如果使用栈,对于B的处理逻辑,我们需要依靠A,也就是说我们可以依靠过去的值来处理现在的逻辑
这是我理解中栈与队列的选型最大的差别。

你可能感兴趣的:(数据结构与算法探究)