题目描述(困难难度)

简单计算器,只有加法和减法以及括号,并且参与运算的数字都是非负数。

思路分析

科学计算器的话,学栈的时候当时一定会遇到的一个练手项目了。记得当时自己写了黑框的计算器,QT 版的计算器,安卓版的计算器,难点就是处理优先级、括号、正负数的问题,几年过去自己也只记得大体框架了,当时用了两个栈,然后遇到操作数怎么办,遇到操作符怎么办,遇到括号怎么办,总之有一个通用的方法,下边的思路也没有细讲了,直接网上搜到了压栈出栈的过程,然后写了相应的代码供参考。解法三的话算作对这道题专门的解法。

解法一 逆波兰式

的时候我们做了逆波兰数。

我们平常用的是中缀表达式,也就是上边 Explanation 中解释的。题目中的是逆波兰式,也叫后缀表达式,一个好处就是只需要运算符,不需要括号,不会产生歧义。

计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。

然后当时直接用栈写了代码,遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。

有了上边的代码,我们只需要把题目给的中缀表达式转成后缀表达式,直接调用上边计算逆波兰式就可以了。

中缀表达式转后缀表达式也有一个通用的方法,我直接复制 这里 的规则过来。

1)如果遇到操作数,我们就直接将其加入到后缀表达式。

2)如果遇到左括号,则我们将其放入到栈中。

3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符加入到后缀表达式直到遇到左括号为止,接着将左括号弹出,但不加入到结果中。

4)如果遇到其他的操作符,如(“+”, “-”)等,从栈中弹出元素将其加入到后缀表达式,直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。弹出完这些元素后,最后将当前遇到的操作符压入到栈中。

5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。

这里的话注意一下第四条规则,因为题目中只有加法和减法,加法和减法是同优先级的,所以一定不会遇到更低优先级的元素,所以「直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。」这句话可以改成「直到遇到左括号或者栈为空为止」。

然后就是对数字的处理,因为数字可能并不只有一位,所以遇到数字的时候要不停的累加。

当遇到运算符或者括号的时候就将累加的数字加到后缀表达式中。

  1. String[] polish = getPolish(s); //转后缀表达式
  2. return evalRPN(polish);
  3. }
  4. //中缀表达式转后缀表达式
  5. private String[] getPolish(String s) {
  6. List<String> res = new ArrayList<>();
  7. Stack<String> stack = new Stack<>();
  8. char[] array = s.toCharArray();
  9. int n = array.length;
  10. int temp = -1; //累加数字,-1 表示当前没有数字
  11. for (int i = 0; i < n; i++) {
  12. if (array[i] == ' ') {
  13. continue;
  14. }
  15. //遇到数字
  16. if (isNumber(array[i])) {
  17. //进行数字的累加
  18. if (temp == -1) {
  19. temp = array[i] - '0';
  20. } else {
  21. temp = temp * 10 + array[i] - '0';
  22. }
  23. } else {
  24. //遇到其它操作符,将数字加入到结果中
  25. if (temp != -1) {
  26. res.add(temp + "");
  27. temp = -1;
  28. }
  29. if (isOperation(array[i] + "")) {
  30. //遇到操作符将栈中的操作符加入到结果中
  31. while (!stack.isEmpty()) {
  32. //遇到左括号结束
  33. if (stack.peek().equals("(")) {
  34. break;
  35. }
  36. res.add(stack.pop());
  37. }
  38. //当前操作符入栈
  39. stack.push(array[i] + "");
  40. } else {
  41. //遇到左括号,直接入栈
  42. if (array[i] == '(') {
  43. stack.push(array[i] + "");
  44. }
  45. //遇到右括号,将出栈元素加入到结果中,直到遇到左括号
  46. if (array[i] == ')') {
  47. while (!stack.peek().equals("(")) {
  48. res.add(stack.pop());
  49. }
  50. //左括号出栈
  51. stack.pop();
  52. }
  53. }
  54. }
  55. }
  56. //如果有数字,将数字加入到结果
  57. if (temp != -1) {
  58. res.add(temp + "");
  59. }
  60. //栈中的其他元素加入到结果
  61. while (!stack.isEmpty()) {
  62. res.add(stack.pop());
  63. }
  64. String[] sArray = new String[res.size()];
  65. //List 转为 数组
  66. for (int i = 0; i < res.size(); i++) {
  67. sArray[i] = res.get(i);
  68. }
  69. return sArray;
  70. }
  71. // 下边是 150 题的代码,求后缀表达式的值
  72. public int evalRPN(String[] tokens) {
  73. Stack<String> stack = new Stack<>();
  74. for (String t : tokens) {
  75. if (isOperation(t)) {
  76. int a = stringToNumber(stack.pop());
  77. int b = stringToNumber(stack.pop());
  78. int ans = eval(b, a, t.charAt(0));
  79. } else {
  80. stack.push(t);
  81. }
  82. }
  83. return stringToNumber(stack.pop());
  84. }
  85. private int eval(int a, int b, char op) {
  86. switch (op) {
  87. case '+':
  88. return a + b;
  89. case '-':
  90. return a - b;
  91. case '*':
  92. return a * b;
  93. case '/':
  94. return a / b;
  95. }
  96. return 0;
  97. }
  98. private int stringToNumber(String s) {
  99. int sign = 1;
  100. int start = 0;
  101. if (s.charAt(0) == '-') {
  102. sign = -1;
  103. }
  104. int res = 0;
  105. for (int i = start; i < s.length(); i++) {
  106. res = res * 10 + s.charAt(i) - '0';
  107. }
  108. return res * sign;
  109. }
  110. private boolean isNumber(char c) {
  111. return c >= '0' && c <= '9';
  112. }
  113. private boolean isOperation(String t) {
  114. return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/");
  115. }

解法二 双栈

解法一经过了一个中间过程,先转为了后缀表达式然后进行求值。我们其实可以直接利用两个栈,边遍历边进行的,这个方法是我当时上课学的方法。从 把过程贴到下边,和解法一其实有些类似的。

  1. 使用两个栈,stack0 用于存储操作数,stack1 用于存储操作符
  2. 从左往右扫描,遇到操作数入栈 stack0
  3. 遇到操作符时,如果当前优先级低于或等于栈顶操作符优先级,则从 stack0 弹出两个元素,从 stack1 弹出一个操作符,进行计算,将结果并压入stack0,继续与栈顶操作符的比较优先级。
  4. 如果遇到操作符高于栈顶操作符优先级,则直接入栈 stack1
  5. 遇到左括号,直接入栈 stack1
  6. 遇到右括号,则从 stack0 弹出两个元素,从 stack1 弹出一个操作符进行计算,并将结果加入到 stack0 中,重复这步直到遇到左括号

和解法一一样,因为我们只有加法和减法,所以这个流程可以简化一下。

第 3 条改成「遇到操作符时,则从 stack0 弹出两个元素进行计算,并压入stack0,直到栈空或者遇到左括号,最后将当前操作符压入 stack1

整体框架和解法一其实差不多,数字的话同样也需要累加,然后当遇到运算符或者括号的时候就将数字入栈。

有一点需要注意,就是算减法的时候,是 num2 - num1,因为我们最初压栈的时候,被减数先压入栈中,然后减数再压栈。出栈的时候,先出来的是减数,然后才是被减数。

解法三

当然,因为只有加法和减法,所以可以不用上边通用的的方法,可以单独分析一下。

首先,将问题简单化,如果没有括号的话,该怎么做?

1 + 2 - 3 + 5

我们把式子看成下边的样子。

+ 1 + 2 - 3 + 5

用一个变量 op 记录数字前的运算,初始化为 +。然后用 res 进行累加结果,初始化为 0。用 num 保存当前的操作数。

从上边第二个加号开始,每次遇到操作符的时候,根据之前保存的 op 进行累加结果 res = res op num,然后 op 更新为当前操作符。

结合代码理解一下。

  1. public int calculateWithOutParentheses(String s) {
  2. char[] array = s.toCharArray();
  3. int n = array.length;
  4. int res = 0;
  5. int num = 0;
  6. char op = '+';
  7. for (int i = 0; i < n; i++) {
  8. if (array[i] == ' ') {
  9. continue;
  10. }
  11. if (array[i] >= '0' && array[i] <= '9') {
  12. num = num * 10 + array[i] - '0';
  13. } else {
  14. if (op == '+') {
  15. res = res + num;
  16. }
  17. if (op == '-') {
  18. res = res - num;
  19. }
  20. num = 0;
  21. op = array[i];
  22. }
  23. }
  24. if (op == '+') {
  25. res = res + num;
  26. }
  27. if (op == '-') {
  28. res = res - num;
  29. }
  30. return res;
  31. }

下边考虑包含括号的问题。

可能是这样 1 - (2 + 4) + 1,可能括号里包含括号 2 + (1 - (2 + 4)) - 2

做法也很简单,当遇到左括号的时候,我们只需要将当前累计的结果,以及当前的 op 进行压栈保存,然后各个参数恢复为初始状态,继续进行正常的扫描计算。

当遇到右括号的时候,将栈中保存的结果和 op 与当前结果进行计算,计算完成后将各个参数恢复为初始状态,然后继续进行正常的扫描计算。

举个例子,对于 2 + 1 - (2 + 4) + 1,遇到左括号的时候,我们就将已经累加的结果 3 和左括号前的 - 放入栈中。也就是 3 - (...) + 1

接着如果遇到了右括号,括号里边 2 + 4 的结果是 6,已经算出来了,接着我们从栈里边把 3- 取出来,也就是再计算 3 - 6 + 1 就可以了。

结合代码再看一下。

参考 这里,我们可以将代码简化一些。上边计算的时候,每次都判断当前 op 是加号还是减号,比较麻烦。我们可以将两者统一起来。用一个变量 sign 代替 op。如果是 +sign 就等于 1。如果是 -sign 就等于 -1

这样做的好处就是,更新 res 的时候,两种情况可以合为一种, res = res + sign * num

另外一个好处就是,我们不再需要两个栈。因为此时的 sign 也是 int 类型,所以可以把它和 res 放到同一个栈中。

  1. public int calculate(String s) {
  2. char[] array = s.toCharArray();
  3. int n = array.length;
  4. int res = 0;
  5. int num = 0;
  6. Stack<Integer> stack = new Stack<>();
  7. int sign = 1;
  8. for (int i = 0; i < n; i++) {
  9. if (array[i] == ' ') {
  10. if (array[i] >= '0' && array[i] <= '9') {
  11. num = num * 10 + array[i] - '0';
  12. } else if (array[i] == '+' || array[i] == '-') {
  13. res = res + sign * num;
  14. //将参数重置
  15. num = 0;
  16. sign = array[i] == '+' ? 1 : -1;
  17. // 遇到左括号,将结果和括号前的运算保存,然后将参数重置
  18. } else if (array[i] == '(') {
  19. stack.push(res);
  20. stack.push(sign);
  21. sign = 1;
  22. res = 0;
  23. } else if (array[i] == ')') {
  24. // 将右括号前的运算结束
  25. res = res + sign * num;
  26. // 将之前的结果和操作取出来和当前结果进行运算
  27. int signBefore = stack.pop();
  28. int resBefore = stack.pop();
  29. res = resBefore + signBefore * res;
  30. // 将参数重置
  31. sign = 1;
  32. num = 0;
  33. }
  34. }
  35. res = res + sign * num;
  36. return res;
  37. }

解法四

这道题的关键就是怎么处理括号的问题,如果我们把括号中的结果全算出来,然后再计算整个表达式也就不难了。

比如 2 - (6 + 5 + 2) + 4,把括号中的结果得到,然后计算 2 - 13 + 4 就很简单了。

括号匹配问题的话,自然会想到栈,比如 的括号匹配。

这里的话,我们当然也使用栈,当出现匹配的括号的时候,就计算当前栈中所匹配的括号里的表达式。

换句话讲,遍历表达式一直将元素入栈,直到我们遇到右括号就开始出栈,一直出栈直到栈顶是左括号,这期间出栈的元素就是当前括号中的表达式。

举个例子。

遗憾的时候,会发现我们计算结果是错误的。原因就是减法不满足交换律,由于我们使用了栈,所以会使得计算倒过来。A + B 变成 B + A 没什么问题,但是 A - B 变成 B - A 就会出问题了。

解决这个问题也很简单,我们只需要倒着遍历原表达式就可以了,相当于先把 A - B 变成了 B - A ,通过栈运算的话,我们就是计算 A - B 了。

然后因为是倒着遍历,所以我们先会遇到右括号,然后是左括号。所以算法变成了遇到左括号后开始出栈进行表达式的计算。

还有个问题需要解决,因为我们是倒着遍历,对于有好几位的数字,我们先得到的是数字的低位,最后得到的是数字的高位,所以数字的更新方式和之前的算法都不同。

举个例子,对于 123,初始化 num = 0

由于是倒着遍历,我们先会得到 3,此时 num = 3 * 1 + num = 3

然后得到 2,此时 num = 2 * 10 + num = 23

然后得到 1,此时 num = 1 * 100 + num = 123

也就是每次得到数要依次乘 110100 … 之后再与原结果累加。

  1. public int calculate(String s) {
  2. char[] array = s.toCharArray();
  3. int n = array.length;
  4. Stack<Integer> stack = new Stack<>();
  5. int num = 0;
  6. int pow = 1;
  7. for (int i = n - 1; i >= 0; i--) {
  8. if (array[i] == ' ') {
  9. continue;
  10. }
  11. if (array[i] >= '0' && array[i] <= '9') {
  12. num = (array[i] - '0') * pow + num;
  13. pow *= 10;
  14. } else {
  15. //当前是否有数字
  16. if (pow != 1) {
  17. stack.push(num);
  18. num = 0;
  19. pow = 1;
  20. }
  21. //使用解法三的技巧, 加号用 1 表示, 减号用 -1 表示
  22. if (array[i] == '+' || array[i] == '-') {
  23. stack.push(array[i] == '+' ? 1 : -1);
  24. //遇到左括号开始计算栈中元素
  25. } else if (array[i] == '(') {
  26. int res = evaluateExpr(stack);
  27. //右括号出栈
  28. stack.pop();
  29. //括号内计算的结果入栈
  30. stack.push(res);
  31. } else if (array[i] == ')') {
  32. // 将右括号入栈,用 -2 表示
  33. stack.push(-2);
  34. }
  35. }
  36. }
  37. //当前是否有数字
  38. if (pow != 1) {
  39. stack.push(num);
  40. }
  41. //计算去除完括号以后栈中表达式的值
  42. return evaluateExpr(stack);
  43. }
  44. private int evaluateExpr(Stack<Integer> stack) {
  45. //第一个数作为初始结果
  46. int res = stack.pop();
  47. //栈不空,并且没有遇到右括号
  48. while (!stack.isEmpty() && stack.peek() != -2) {
  49. //第一个出栈的元素是操作符,第二个出栈的元素是操作数
  50. res = res + stack.pop() * stack.pop();
  51. }
  52. return res;
  53. }

解法一和解法二算是通用的方法,也就是加上乘除运算以后方法依旧通用。

解法三的话,就是针对这道题进行的一个简化的算法,最关键的就是括号的处理,栈的应用很关键。然后就是一个技巧,通过 sign 将加减统一起来很漂亮。

解法四的话很巧妙,但不容易想到,通过栈找到匹配的括号,然后先计算括号中的元素,最终等效于先把所有括号去掉再统一计算主表达式,相当于主表达式延迟了计算,倒是很符合我们平常计算带括号的表达式的思维。「有括号的,先计算括号里边的」,想起了小学时候的口诀,哈哈。

windliang wechat

添加好友一起进步~

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情