基本计算器

512 阅读6分钟

1.1 问题描述

实现一个基本的计算器来计算一个简单的字符串表达式 s 的值。

示例 1:

​ 输入:s = "1 + 1" ​ 输出:2

示例 2:

​ 输入:s = " 2-1 + 2 " ​ 输出:3

1.2 思路及复杂度分析

因为睡眠不好的原因,总觉得头胀胀的,精神状态不好,但每日算法的断更又是我所不能接受的。所以这一篇我就用了力扣官方的题解,来为大家解析一下思路并做适当延伸。

题目要求满足+/-两种操作符的运算,且操作数都为非负整数,而且包含括号

首先明确的是,对于这种表达式求值的问题,编译器对于表达式求值都是通过栈来实现的,我们这里也不例外。

另外小学的知识告诉我们有括号的话,应该先算括号里面的表达式,再将子表达式求解的结果作为表达式的一部分继续运算。此外,还有一个容易忽略的地方,那就是空格!!!很多人出于美观的编程习惯,会在表达式中加入空格,这样对表达式的求解并没有影响,但我们在编程时需要刻意的忽略空格。最后,需要注意的还有对操作数的处理,在一个字符串中123被认定为三个字符!我们需要使用十进制的运算知识将三个字符组合在一起处理。

到此,基本计算器的核心部分就完结了,具体的代码看最后的代码演示部分就ok了。

您可能会说,我点进来你就给我看这个,不是说好有干货吗~

的确,在我看来呢,掌握上面的的确可以解决问题,但它不能作为一种解决问题的通用模式,依靠这种灵光一现的技巧是不长远的,对于表达式求值的问题,更通用的解法是逆波兰表达式——

刚好力扣上有这么一道题,我们来一起小试牛刀吧~

逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。

有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

思路及复杂度分析


在分析之前,有必要先聊聊逆波兰表达式是什么,为什么它对于表达式的求解有帮助?

逆波兰表达式是一种利用栈来进行运算的数学表达式。我们通常使用的表达式是中缀表达式,比如:

(1+2)(34)(1+2)*(3-4)

计算机求解时会将这个中缀表达式先解析为符号树,再求值。而解析符号树需要递归遍历这颗树,假如表达式比较复杂,也就是树的深度大时,会申请大量栈空间性能不高,而如果将中缀表达式变成后缀表达式时,情况就有所不同了,只需要很小的栈空间就可以完成操作。

说到这,你可能还是似懂非懂,同样的表达式内容,为什么逆波兰表达式效率就高呢~

这是因为逆波兰表达式可以简化表达式的内容,它设计的初衷就是因为可以省略括号,从而减少CPU运算的次数。


既然逆波兰表达式这么好用,那怎么将中缀表达式转化为逆波兰表达式呢?那就要看调度车算法咯——

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于找顶符号(乘除优先加减)则栈顶元素依次出找并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。

对于其具体的案例,可以移步这篇博文:www.nowamagic.net/librarys/ve…


看到这的小伙伴可以舒一口气了,内功已成,剩下的招式就是信手拈来啦。但是追求更好的性能是一个coder应该有的习惯,所以我们会从数组模拟栈两种思路来求解——

栈求解

它的思路可谓非常朴素,我们先遍历数组,将所有的操作数入栈,当遇到运算符时,就取出栈顶的两个元素,并将运算的结果压栈,

将遍历得到的字符串转化为整数时,有一个小细节值得注意:是使用Integer.parseInt()还是Integer.ValueOf(),它们都可以将字符串转化为整数,但前者比起后者少了装箱的操作.

image-20210216234940724

数组模拟栈求解

虽然栈提供了对出栈入栈操作提供了方便的封装,但也导致其操作不灵活,就拿这句代码来看——

image-20210216235301216

完全可以用更简洁的数组代码实现:

image-20210216235403849

此外,只需要申请一个数组用于保存操作数即可,而假设一个表达式长度为n,则最怀情况下,这个表达式中只包含操作数,并且操作数只有一个,也就是

n/2+1n/2+1

趣味图解

图片来自力扣大佬的题解:leetcode-cn.com/problems/ev…

image.png

代码演示

package com.zyz.Stack;

/**
 * 逆波兰表达式
 *
 */
public class Solution2 {
    public int evalRPN(String[] tokesns) {
        //申请一个存储操作数的数组
        int[] res = new int[tokesns.length/2+1];
        int index = 0;
        //遍历元素,如果是数字,则放到数组中
        for (String str:tokesns) {
            switch (str){
                case "+":
                    res[index-2] += res[--index];
                    break;
                case "-":
                    res[index-2] -= res[--index];
                    break;
                case "*":
                    res[index-2] *= res[--index];
                    break;
                case "/":
                    res[index-2] /= res[--index];
                    break;
                default:
                    res[index++] = Integer.parseInt(str);
                    Integer.valueOf(str)
                    break;
            }
        }

        return res[0];
    }
}


class Solution {
    public int evalRPN(String[] tokesns) {
         Stack<Integer> stack = new Stack<>();
        int op1 = 0;
        int op2 = 0;
        for(String str:tokesns){
           switch (str){
               case "+":
                   op1 = stack.pop();
                   op2 = stack.pop();
                   stack.push(op1+op2);
                   break;
               case "-":
                   op1 = stack.pop();
                   op2 = stack.pop();
                   stack.push(op2-op1);
                   break;
               case "*":
                   op1 = stack.pop();
                   op2 = stack.pop();
                   stack.push(op1*op2);
                   break;
               case "/":
                   op1 = stack.pop();
                   op2 = stack.pop();
                   stack.push(op2/op1);
                   break;
               default:
                   stack.push(Integer.parseInt(str));
                   break;
           
           }
       }
        return stack.pop();
    }
}

1.3 代码演示

class Solution {
    public int calculate(String s) {
        //为了避免运算时数据类型无法转换,选用Object
        Stack<Object> stack = new Stack<>();
        int operand = 0;
        int n = 0;
        //逆序字符串
        for(int i=s.length()-1;i>=0;--i){
            //对入栈的字符进行判断
            char ch = s.charAt(i);
            //数字的话,则组合
            if(Character.isDigit(ch)){
                operand += (int)Math.pow(10,n)*(int)(ch-'0');
                ++n;
            }else if(ch != ' '){//不是空格,也不是数字,则应该是操作符和操作数
                //前置处理,逻辑追加:清0操作数,清零计数
                if(n != 0){
                    stack.push(operand);
                    n = 0;
                    operand = 0;
                }
                //左括号
                if(ch == '('){//左括号的话,运算栈中子表达式的结果直到碰到右括号,弹栈,压栈
                    int res = calculateExp(stack);
                    stack.pop();
                    stack.push(res);

                }else{//右括号,或者+-操作符
                    stack.push(ch);
                }
            }
        }
        if(n != 0)
            stack.push(operand);
        return calculateExp(stack);
    }

    private static int calculateExp(Stack<Object> stack) {
        int res = 0;
        //特判,非空
        if(!stack.empty())
            res = (int)stack.pop();
        while(!stack.empty() && (char)stack.peek()!=')'){
            //+号运算符
            char ch = (char)stack.pop();
            if('+' == ch){
                res += (int)stack.pop();
            }else{
                res -= (int)stack.pop();
            }
        }
        return res;
    }
}

日拱一卒,功不唐捐。