我们的目标是写一个表达式求值的计算器。我今天先介绍一种做法,那就是使用栈来完成表达式求值。
什么是数据结构
栈是一种数据结构。那什么是数据结构呢?我这里不给出严格的定义,因为对于完全没有基础的新人而言,严格的定义说了等于没说。我只从一个简单的角度举例说明一下。在初学阶段,你可以认为数据结构就是关于数据在计算机中如何组织的一门课程。
比如,我要往一个数组中,存1,3,2这三个整数,那么,我实际存的时候,是按照从小到大排着存呢,还是从大到小存,还是没有顺序随便存,这要根据实际的需求来决定。根据需求决定数据存储的方式。这就是数据结构要研究的内容。
什么是栈
以前打了粮食,会放到一个筒形的容器里,这个筒形的容器,大家叫它栈。它的特点是先放进去的粮食要最后才能取出来,后放进去的粮食是最先被取出来。所以中文就用栈这个词翻译英文的stack了。
想一想,生活中,还有很多栈的例子。比如叠在一起的碗盘,叠的时候我们是从底往高处叠,但是取的时候,我们是从最上面的一个依次向下取。这也是一个典型的栈:后进先出,先进后出。
那么Java语言中如何模拟这一个过程呢?首先,我们定义一个名叫Stack的类:
public
因为我们的课程里还没有涉及到泛型,为了简单起见,我先用整型代替了。如果有些同学,已经有一定的基础了,也可以看一下泛型版的代码:
import java.util.ArrayList;
public class Stack<T> {
private ArrayList<T> list;
private int size;
public Stack(int size) {
this.size = size;
this.list = new ArrayList<T>(size);
}
public static void main(String args) {
Stack<Integer> s = new Stack<Integer>(10);
}
}
本文中后面的例子,我还是会使用简单版本的。
OK,接下来,就是要往类里添加相关的操作了。先定义一个可以往栈里写数据的函数,叫做push:
public
这个函数的意义就是,把数据num存到数组里,并且把游标向后指一位。逻辑比较简单。
相对应的,我们还可以继续定义以下三个方法,分别是取栈顶元素(getTop),出栈(pop),判断栈是否为空(isEmpty)。
public
好了,一个栈就定义完了(当然,这里是有问题的,因为我们在push里,没有检查top是否越过了size规定的数组,出栈的时候,也没有判断top是否等于0。这个做为今天的第一个作业,请读者自行添加。)
栈的应用
这个数据结构看上去好简单啊。肯定有很多人会这样想,但其实,栈在计算机编程中可以说是最基础也是最重要的数据结构了。其功能之强大,可能出乎很多人的意料。我们先通过一个小例子来体会一下。看这样一道题目:
输入一组括号,请判断这些括号的匹配是否合法。例如
(()],不合法,左边的小括号与右边的中括号不能匹配。
{[()]},合法的,所有的括号都可以正确匹配。
{(}),不合法,顺序是错的。
((()),不合法,右括号少了一个。
大家可以先自己想一下,这个问题怎么解决,自己写一写,看看能不能搞得定。如果不使用栈,自己穷举所有情况,逐个去处理的话,好像有点麻烦。
我们来分析一下。如果遇到第一个右括号,那与之配对的一定是离它最近的那个左括号,如果离它最近的左括号的类型与这个右括号的类型是一样的(比如都是小括号),那这就是一次成功的配对。把一组成功配对的括号从括号序列中删去,不会影响原来序列是否合法这个属性,就是说原来合法的,仍然合法,原来不合法的,仍然不合法。通过这样的办法就可以化简题目了。
算法是有了,怎么实现呢?遇到右括号,只去检查离它最近的,如果匹配上了,就把左右括号一起删掉,这不就是栈吗?每次都只检查栈顶的左括号,如果与右括号匹配上了,就把左括号出栈(删掉)。
好了,数据结构有了,算法有了,写成程序就太简单了:
public
虽然代码很长,但其实逻辑非常简单易懂。这里漏了一种情况,那就是左括号少,右括号多的情况。这个也留做作业(其实就是getTop和pop的时候要做检查)
Java虚拟机的实现是基于栈的
Java虚拟机规范里定义了Java所使用的字节码。我们知道Java文件要先编译成字节码文件,也就是class文件,但是大家有没有想过,class文件里都是些什么呢?class文件的结构,我以后会专门讲。今天只演示一个简单的例子,让大家有个初步的感觉。先看这样一个源文件:
public class Main { // Main.java
public static int add(int a, int b){
return a + b;
}
}
执行以下命令:
javac Main.java
javap -c Main
可以看到这样的输出:
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
add 函数被编成了四条字节码指令,这四条字节码是什么意思呢?
你可以认为Java的每一个函数都有一个操作数栈,每条指令就是在对这个操作数栈进行操作。比如 iload_0,就代表把第一个参数 push 进操作数栈,iload_1代表把第二个参数 push 进操作数栈,而 iadd 代表,从栈上连续pop两次,取两个数,将其相加,再把结果送到栈上。ireturn 则表示把栈顶的值做为返回值传给调用者。Java字节码的整个执行过程都是在这样一个栈上的。如果用图来表示,这四个步骤就是:
iload_0,使a先进栈
iload_1, 使b进栈
iadd,做了三件事情,b 和 a 出栈,计算 a+b,将计算结果入栈:
ireturn 将当前栈顶的值返回出去。
最后一节看不懂也没有关系。我们后面会有Java虚拟机的专门讲解。好了,今天的课程就到这里了。
作业:
1. 实现完整版的 stack
2. 完善文章中的代码,使得(()))这种情况也能正确执行。
3. 后缀表达式是这样的一种表达式:
不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则,如:(2 + 1) * 3 , 即2 1 + 3 *
请结合上一节课的TokenStream的基础上,用栈实现后缀表达式求值 ,例如,输入是
8 5 - 4 2 - *
输出值是6,计算过程是8-5为3,4-2为2,3与2相乘得6