简介
在学习stack的时候,我们知道这个数据结构在很多地方有一个很好的应用。比如编程语言中的方法调用。另外,在一些算术表达式求值的过程中,对它的应用也比较巧妙。
一个求表达式的问题
假设我们有一个算术表达式,比如((1 + (2 * 3)) / (4 + 5))。在这里,表达式是一个字符串。我们需要解析出来其中常用的运算符号比如+, -, *, /。同时,也要根据括号定义的运算顺序计算出一个合理的结果。
分析
对于这个问题,该怎么来分析呢?按照以前的文章分析,这里所有表达式都是按照括号给包含起来的,从递归定义的角度来说,它是一个(a op b)的形式。其中a, b表示为一个子表达式。它们也可能是一个数字。以前面的示例来说,我们如果要计算表达式((1 + (2 * 3)) / (4 + 5))的值,实际计算的过程如下:
((1 + (2 * 3)) / (4 + 5)) => (( 1 + 6) / (4 + 5) ) => (7 / (4 + 5)) => (7 / 9) => 0.77
从前面的计算过程里,我们如果仔细观察的话,会发现为了消除一组括号,就必须每次将符合(a op b)这样的算式给合并。这里的a和b是最基本的数字。
按照这一步来看,我们可以首先计算的是(2 * 3),我们也可以直接计算(4 + 5)。这个时候该怎么取舍呢?毕竟要消除的是一组括号。消除它的条件是当碰到一个右括号的时候,就取这个算式里左边的两个数字和一个运算符号进行计算。如果按照从左到右的先后顺序,我们可以推导出一个和前面演示过程一样的步骤。这个步骤概括起来就是:
1. 从左到右遍历,碰到有右括号的时候,取到和右括号对应的左括号之间的元素进行运算。
这样,一个基本的表达式就可以消除了。
可是,上述的这个过程还是有一些模糊的地方。我们该怎么来记录这个左括号和右括号之间的元素呢?而且还包括有运算符。如果碰到右括号的时候,我们需要往回去取。所以这个时候可以考虑使用栈来保存原来的数据。当碰到右括号就从栈弹出几个元素来进行运算。
按照这种思路,我们可以用一个栈来处理整个表达式,它的过程如下:
1. 遍历表达式元素
2. 如果不是右括号,元素直接入栈,对于左括号,可以直接忽略
3. 如果是右括号,弹出栈顶的3个元素,分别对应a, op, b。
4. 计算上述的表达式结果,然后将该结果压栈。
按照这种思路,我们可以很容易的得出实现代码来了。这里就不详细贴代码了。
上面说的这种思路是将数字和运算符作为字符放到一个栈里来处理。实际上,如果我们稍微做一点变通,再使用一个额外的运算符栈,只是用来保存运算符。而原来的栈只保存运算的数字。我们可以得到一个和前面思路相近的变体。它们的差别在于,每次我们碰到右括号的时候,直接从运算符栈里弹出栈顶的元素,然后再从运算数据的栈里取出最上面的两个元素。和前面的方法实现比起来,这种方法要显得更加简单直观一些。一个参考的实现如下:
import java.util.Scanner;
import java.util.Stack;
public class Evaluate {
public static void main(String[] args) {
Stack<String> ops = new Stack<String>();
Stack<Double> vals = new Stack<Double>();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
String s = scanner.nextLine();
if(s.equals("("));
else if(s.equals("+")) ops.push(s);
else if(s.equals("-")) ops.push(s);
else if(s.equals("*")) ops.push(s);
else if(s.equals("/")) ops.push(s);
else if(s.equals("sqrt")) ops.push(s);
else if(s.equals(")")) {
String op = ops.pop();
double v = vals.pop();
if(op.equals("+")) v = vals.pop() + v;
else if(op.equals("-")) v = vals.pop() - v;
else if(op.equals("*")) v = vals.pop() * v;
else if(op.equals("/")) v = vals.pop() / v;
else if(op.equals("sqrt")) v = Math.sqrt(v);
vals.push(v);
}
else vals.push(Double.parseDouble(s));
}
System.out.println(vals.pop());
}
}
总结
使用栈来求一些算术表达式在编译原理等场景中很常见。关键在于要理顺它们的求值顺序,考虑利用一些括号的对称以及表达式本身递归的定义等特性。这里讨论的是中缀表达式的求值。实际上对于前缀表达式以及后缀表达式的求值也有很多有意思的方法值得我们去探讨。