什么是数据结构
栈是一种数据结构。那什么是数据结构呢?我这里不给出严格的定义,因为对于完全没有基础的新人而言,严格的定义说了等于没说。我只从一个简单的角度举例说明一下。在初学阶段,你可以认为数据结构就是关于数据在计算机中如何组织的一门课程。
比如,我要往一个数组中,存1,3,2这三个整数,那么,我实际存的时候,是按照从小到大排着存呢,还是从大到小存,还是没有顺序随便存,这要根据实际的需求来决定。根据需求决定数据存储的方式。这就是数据结构要研究的内容。
什么是栈
以前打了粮食,会放到一个筒形的容器里,这个筒形的容器,大家叫它栈。它的特点是先放进去的粮食要最后才能取出来,后放进去的粮食是最先被取出来。所以中文就用栈这个词翻译英文的stack了。
想一想,生活中,还有很多栈的例子。比如叠在一起的碗盘,叠的时候我们是从底往高处叠,但是取的时候,我们是从最上面的一个依次向下取。这也是一个典型的栈:后进先出,先进后出。
那么Java语言中如何模拟这一个过程呢?首先,我们定义一个名叫Stack的类:
public class Stack { private int[] data; private int size; private int top = 0; // 指向栈的顶部 public Stack(int size) { this.size = size; this.data = new int[size]; } }
因为我们的课程里还没有涉及到泛型,为了简单起见,我先用整型代替了。如果有些同学,已经有一定的基础了,也可以看一下泛型版的代码:
import java.util.ArrayList;public class Stack { private ArrayList list; private int size; public Stack(int size) { this.size = size; this.list = new ArrayList(size); } public static void main(String args) { Stack s = new Stack(10); } }
本文中后面的例子,我还是会使用简单版本的。
OK,接下来,就是要往类里添加相关的操作了。先定义一个可以往栈里写数据的函数,叫做push:
public void push(int num) { data[top++] = num; }
这个函数的意义就是,把数据num存到数组里,并且把游标向后指一位。逻辑比较简单。
相对应的,我们还可以继续定义以下三个方法,分别是取栈顶元素(getTop),出栈(pop),判断栈是否为空(isEmpty)。
public int pop(int num) { return data[--top]; } public int getTop(int num) { return data[top-1]; } public boolean isEmpty() { return top == 0; }
好了,一个栈就定义完了(当然,这里是有问题的,因为我们在push里,没有检查top是否越过了size规定的数组,出栈的时候,也没有判断top是否等于0。这个做为今天的第一个作业,请读者自行添加。)
栈的应用
这个数据结构看上去好简单啊。肯定有很多人会这样想,但其实,栈在计算机编程中可以说是最基础也是最重要的数据结构了。其功能之强大,可能出乎很多人的意料。我们先通过一个小例子来体会一下。看这样一道题目:
输入一组括号,请判断这些括号的匹配是否合法。例如
(()],不合法,左边的小括号与右边的中括号不能匹配。
{[()]},合法的,所有的括号都可以正确匹配。
{(}),不合法,顺序是错的。
((()),不合法,右括号少了一个。
大家可以先自己想一下,这个问题怎么解决,自己写一写,看看能不能搞得定。如果不使用栈,自己穷举所有情况,逐个去处理的话,好像有点麻烦。
我们来分析一下。如果遇到第一个右括号,那与之配对的一定是离它最近的那个左括号,如果离它最近的左括号的类型与这个右括号的类型是一样的(比如都是小括号),那这就是一次成功的配对。把一组成功配对的括号从括号序列中删去,不会影响原来序列是否合法这个属性,就是说原来合法的,仍然合法,原来不合法的,仍然不合法。通过这样的办法就可以化简题目了。
算法是有了,怎么实现呢?遇到右括号,只去检查离它最近的,如果匹配上了,就把左右括号一起删掉,这不就是栈吗?每次都只检查栈顶的左括号,如果与右括号匹配上了,就把左括号出栈(删掉)。
好了,数据结构有了,算法有了,写成程序就太简单了:
public static void main(String args[]) throws IOException { byte[] buf = new byte[128]; int length = System.in.read(buf); Stack s = new Stack(128); for (int i = 0; i < length; i++) { if (buf[i] == '(' || buf[i] == '[' || buf[i] == '{') { s.push(buf[i]); } else if (buf[i] == ')') { if (s.getTop() == '(') s.pop(); else { System.out.println("1 unmatch!"); System.exit(1); } } else if (buf[i] == ']') { if (s.getTop() == '[') s.pop(); else { System.out.println("2 unmatch!"); System.exit(1); } } else if (buf[i] == '}') { if (s.getTop() == '{') s.pop(); else { System.out.println("3 unmatch!"); System.exit(1); } } } if (!s.isEmpty()) System.out.println("4 unmatch!"); else System.out.println("matched~"); }
虽然代码很长,但其实逻辑非常简单易懂。这里漏了一种情况,那就是左括号少,右括号多的情况。这个也留做作业(其实就是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.javajavap -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先进栈
![57e21a0cbb28bbc9058604bbdc40939d.png](https://i-blog.csdnimg.cn/blog_migrate/b6ef301f0c35a1335ceab6f8cbe146bf.jpeg)
iload_1, 使b进栈
![e3cb01a0cf8e8ea0d796894b4f24d015.png](https://i-blog.csdnimg.cn/blog_migrate/a1209d1b6abd863b5a1b25fc1783b455.jpeg)
iadd,做了三件事情,b 和 a 出栈,计算 a+b,将计算结果入栈:
![8b8e5af13084ba4549f78f6188e6d0d6.png](https://i-blog.csdnimg.cn/blog_migrate/eacc44d758e42dce7ad084f82d36a63c.jpeg)
ireturn 将当前栈顶的值返回出去。