我们的目标是写一个表达式求值的计算器。我今天先介绍一种做法,那就是使用栈来完成表达式求值。

什么是数据结构

栈是一种数据结构。那什么是数据结构呢?我这里不给出严格的定义,因为对于完全没有基础的新人而言,严格的定义说了等于没说。我只从一个简单的角度举例说明一下。在初学阶段,你可以认为数据结构就是关于数据在计算机中如何组织的一门课程。

比如,我要往一个数组中,存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先进栈

java 将中文括号换成英文括号_数据结构括号匹配代码

iload_1, 使b进栈

java 将中文括号换成英文括号_Java_02


iadd,做了三件事情,b 和 a 出栈,计算 a+b,将计算结果入栈:

java 将中文括号换成英文括号_Java_03


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