栈的特征是先进后出,英文缩写为FILO(first in, last out),也就是说所有的添加删除操作,只在尾部进行。根据前面我们的学习,链表、数组列表、循环队列都适合当作栈使用。栈在计算机世界最广泛的应用就是程序函数的调用,因为函数内部还会调用调用,所以是一个树状结构,而函数调用必须先执行最内层的子函数,所以这相当于一个深度优先搜索(DFS)算法,而支撑深度优先搜索算法的核心数据结构就是栈。
这里不讨论深度优先搜索。
先以一个常见的需求表达式解析开始。这里为了简化需求,不考虑代数运算表达式,只考虑数值运算表达式。如下列例子:
1+2*(3+4)-7
这个我用所毕生所学数学知识,计算出来了,结果是8。但是怎么用程序计算呢?因为涉及字符串解析,所以继续简化需求,我把这一步省略了,直接人工拆成了数组。比如java代码如此表示
final Object[] objects = {1, '+', 2, '*', '(', 3, '+', 4, ')', '-', 7};
写代码,什么情况下第一步都是仔细分析需求,先观察输入的序列:
1 + 2 * ( 3 + 4 ) - 7
我看了十遍,得到一个结论,这个序列包含两种不同类型的数据,一种是数字,一种是符号。如果你们一遍就看出来了,那么智商比我高。
我又看了十遍,发现了这个表达式其实是一个二叉树状结构
这棵树很像计算机的函数调用,可以这么说深刻地理解了这棵树,也就理解了计算机的运算步骤了。
从这棵树可以看出一个规律,所有的叶子都是数字,所有的非叶子都是运算符。再回头看看我们的输入顺序。
1 + 2 * ( 3 + 4 ) -7
又发现了一个规律,最先放入的1,并不能马上运算,而是要等待乘法的运算结果。而减号出现后,就必须计算1和右边这棵子树的和了。
规律一:同优先级(其实低优先级也一样)运算符出现后,必须马上运算(加法满足结合律,但是减法不满足结合律,所以必须马上运算);高优先级运算符出现不能运算。
再考虑括号这个东西。括号是用来强制改变优先级的。但是括号也有不同于普通运算符的特征,括号不是一个二元运算符。因为一对括号包着一个子表达式,一个子表达式相当于一个子树。按这个思维,括号算是一个“半元运算符”,左右括号两个运算符才作用于一个数据,平均下来,一个运算符才对应半个数据。
在结合上面的例子分析左右括号的左右。因为有低运算符出现必须马上终止运算的规律,但是左括号的出现,卡在了乘法和加法之间,使得加法树下降了一层,也就是搁置了加法的运算。右括号的出现,标志这一个子表达式的结束,所以必须马上进行运算。
于是我们掌握了一个新的规律
规律二:左括号阻止按优先级的“必须”运算规律,右括号阻止优先级的“搁置”运算规律。
需求分析完了,现在就是选择数据结构了。因为存在搁置运算,先放入的比如图中的数字1,却要最后运算,我们自然想到了栈。又因为数据分两种类型,我们自然想到了用两个栈来做,一个放数据,一个放运算符。
用栈解开表达式很简单。就是两个栈,一个叫操作数栈,一个叫操作符栈。
遇到操作符时,如果优先级低于或等于栈顶操作符优先级,则从操作数栈弹出两个元素进行计算,并压入操作数栈,继续与栈顶操作符的比较优先级,直到遇到左括号或更低优先级运算符为止。
如果遇到的是右括号,需要循环取出栈顶元素进行计算,直到遇到左括号为止。
这个继续比较就是循环比较的意思。
等于是遇到加法呢,要把之前存储的所有乘法和除法计算完毕。
上述算法,就是著名的表达式解析算法:Dijkstra's two-stack algorithm。用我的小学水平英语翻译下就是迪科斯彻双栈算法。
他的发明者就是计算机算法大神,1972年图灵奖得主。
最后贴上Java代码实现:
public class Expression {
private static int getPriority(Character c) {
switch (c) {
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
default:
throw new UnsupportedOperationException("不支持的运算符" + c);
}
}
public static Integer evaluate(Object[] expression) {
Deque<Integer> operand = new ArrayDeque<>();
Deque<Character> operator = new ArrayDeque<>();
for (Object s : expression) {
if (s instanceof Integer) {
operand.addLast((Integer) s);
} else {
Character e = (Character) s;
if ('(' == e) {
operator.addLast(e);
} else if (')' == e) {
//弹出运算符,进行运算,直到弹出(
for (Character op = operator.removeLast(); op != '('; op = operator.removeLast()) {
evaluate(operand, op);
}
} else {
// 看看栈顶的优先级
for (Character peek = operator.peekLast();
peek != null && peek != '(' && getPriority(e) <= getPriority(peek);
peek = operator.peekLast()) {
// 弹出栈进行运算
peek = operator.removeLast();
evaluate(operand, peek);
}
// 运算符压入栈
operator.addLast(e);
}
}
}
while (!operator.isEmpty()) {
Character s = operator.removeLast();
evaluate(operand, s);
}
return operand.pop();
}
private static void evaluate(Deque<Integer> operand, Character s) {
Integer b = operand.removeLast();
Integer a = operand.removeLast();
Integer result = evaluate(a, b, s);
operand.addLast(result);
}
private static Integer evaluate(Integer a, Integer b, Character s) {
switch (s) {
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
return a / b;
default:
throw new UnsupportedOperationException("不支持的运算符" + s);
}
}
}