栈模型
栈是限制插入和删除操作只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶端。对栈的基本操作有push(进栈)和pop(出栈),前者相当于插入,后者则是删除最后的插入元素。
栈的实现
由于栈是一个表,故可以用任何实现表的算法来实现栈,当然,ArrayList和LinkedList都支持栈操作,下面我们给出两种非常流行的实现方法,一种方法使用链式结构,另一种方法使用数组。
栈的链表实现
栈的第一种实现方法是使用单链表。通过在表的顶端插入来实现push,通过删除表顶端元素实现pop。top操作只是考查表顶端元素并返回它的值。有时pop操作和top操作合二为一。
栈的数组实现
另一种实现方法避免了链表而且是最流行的解决方案。模仿ArrayList的add操作来实现栈的方法非常简单。每个栈相关的操作是theArray和topOfStack,对于空栈,topOfStack为-1。为了将某个元素推入栈中,我们使topOfStack增加1后,将theArray[topOfStack]赋予x值,为了弹出栈元素,我们置返回值为theArray[topOfStack]后,然后将topOfStack减1。
注意,这些操作需要一定的边界操作,比如在pop操作时,检查栈是否为空;在push操作时,检查栈是否已满。
栈的应用
平衡符号
在编译器检查程序语法时,比如缺少一个符号(如遗漏一个花括号或注释起始符)等。简单的算法过程如下:
做一个空栈。读入字符直到文件结尾,当读入到的字符是一个开符号,比如[{("'
等,则将其推入栈中。如果读入到的字符是一个闭符号,比如]})"'
,则当栈空时报错;否则将栈元素弹出,如果弹出的元素不是对应的开放符号,则报错。在文件结尾时,如栈非空则报错。
后缀表达式
假设我们要实现一个简单的计算机程序,比如输入的自然表达式是:4.99+5.99+6.99*1.06=? 随着计算机的不同,这个结果的答案也不同,因为有些简单的计算机时逐个进行计算,而较为先进的计算机知道乘法的优先级别高于加法的优先级别。
典型的计算过程可以是先将4.99与5.99相加并存入A1,然后将6.99与1.06相乘并把答案存入A2,最后将A1与A2相加并存入A1。我们可以将这种操作顺序书写为:4.99 5.99 + 6.99 1.06 * +
这种记法叫做后缀或逆波兰记法。计算这种问题最好的方法是使用一个栈,当遇到一个数时就把它推入栈中;在遇到一个运算符时就将该算符作用到从栈顶弹出的两个数上,再将所得的结果推入栈中。比如对于后缀表达式:
6 5 2 3 + 8 * + 3 + *
栈 | 下一个符号 |
---|---|
空 | 6 |
6 | 5 |
6 5 | 2 |
6 5 2 | 3 |
6 5 2 3 | + |
6 5 5 | 8 |
6 5 5 8 | * |
6 5 40 | + |
6 45 | 3 |
6 45 3 | + |
6 48 | * |
288 | 空 |
计算一个后缀表达式所花费的时间时O(N),而且更为重要的是,当一个表达式以后缀号给出的时候,我们没必要知道任何优先级别的规则,这个一个明显的优点。
中缀到后缀的转换
那么如何得到一个后缀表达式呢?这里同样可以使用栈将一个标准的中缀表达式转换为后缀表达式。这里我们只允许操作+,-,*,/,(,),并用普通的优先级法则将一般的问题浓缩为小规模的问题,此外还要假设表达式是合法的,假设中缀表达式为:a + b * c + (d * e + f)* g
将此转换为后缀表达式。
当读到一个操作数的时候,立即把它放到输出中。操作符不立即输出,而是必须先存入到某个栈中,这里我们规定* / + -的优先级别依次降低,其中* 和 /的优先级别系统,而+和-的优先级别相同,最后:除非是在处理一个)的时候,否则我们绝不会从栈中移走(。读入的操纵符,从栈顶中pop出所有优先级别大于或等于该操作符的元素。
最后,如果读入到输入的末尾,我们将栈元素弹出直到该栈变成为空栈,将符号写到输出中
示例如下:
栈 | 输出 | 下一个元素 | |
---|---|---|---|
空 | 空 | a | |
空 | a | + | |
+ | a | b | |
+ | a b | * | *优先级大于+,故不弹出 |
+ * | a b | c | |
+ * | a b c | + | |
+ | a b c * + | ( | +优先级小于*,故*弹出;而栈底的+号与当前操纵符有相同优先级,故弹出;将当前操作符+推入栈中 |
+ ( | a b c * + | d | |
+ ( | a b c * + d | * | |
+ ( * | a b c * +d | e | |
+ ( * | a b c * + d e | + | |
+ ( + | a b c * + d e * | f | |
+ ( + | a b c * + d e * f | ) | |
+ | a b c * + d e * f + | * | 读入直到一个),因此将栈元素直到(弹出 |
+ * | a b c * + d e * f + | g | |
+ * | a b c * + d e * f + g | 空 | |
空 | a b c * + d e * f + g * + | 空 |
方法调用
当存在方法调用时候,需要存储很多重要信息,比如寄存器的值(对应变量的名字)和返回地址等。然后控制转移到新方法,该方法自由地用它的一些值代替这些寄存器。如果它又进行其他的方法调用,那么它也遵循相同的过程。显然,所有全部工作均可由一个栈来完成,而这正是实现递归的每一种程序程序设计语言中实际发生的事实。