本文是在阅读《算法》第四版时的笔记。
栈
栈数据结构遵循 LIFO( last in first out ) 的原则。
它主要有压栈和弹栈这两种操作以及 isEmpty()
和 size()
函数。
LIFO
关于 LIFO 原则,有一个很好的例子:假如你有一堆邮件在桌子上(这堆邮件就是一个栈结构),有新邮件的时候,你会把新邮件放在这一堆邮件的最上面,这个操作就是压栈,新邮件总是处于栈顶,而你要处理这些邮件的时候,会从最上面的开始读,也就是从栈顶开始一封一封的读,读过的邮件,就从这堆邮件里面拿出来,这就是弹栈,弹栈总是对栈顶的数据进行操作。
Java实现一个简单的栈结构
书上目前没看见关于 Stack 具体实现的代码,所以我也是只基于数组实现了 Stack 的基本功能,有不对的地方,请指正,谢谢!!!
这里主要是实现四个方法:
-
Stack()
定义一个栈 -
push(T t)
压栈 -
pop()
弹栈 -
isEmpty()
判断栈是否为空 -
size()
返回栈内数据个数
public class Stack<T> implements Iterable<T> {
@Override
public Iterator<T> iterator() {
return null;
}
private ArrayList<T> array = new ArrayList<T>();
public Stack(){
}
public void push(T t){
this.array.add(t);
}
public T pop(){
return this.array.remove(array.size()-1);
}
public boolean isEmpty(){
return this.array==null;
}
public int size(){
return this.array.size();
}
}
栈的运用
引用《算法》第四版里的例子。
先看一个数学表达式:
( 1 + ( 2 + 3 ) * ( 4 * 5 ) ) )
这个数学表达式,学过小学数学的都知道计算结果是 101 。
我们把上面的表达式输入到计算机里面,输入的是字符串,要怎样处理,它才能得出 101 这个结果呢?
上世纪 70 年代,E.W.Dijkstra 运用两个栈结构解决了这个问题,一个栈 ( ops ) 用来保存操作符(+,-,*,/,sqrt ),一个栈 ( vals ) 用来保存操作数(数字),同时还得遵循下面这几种原则:
- 输入表达式的时候,操作数和操作符要用一个空格隔开。
- 忽略左括号
- 遇见右括号,栈 ops 弹出一个操作符,栈 vals 弹出必要的操作数,然后将计算结果再压栈操作压入栈 vals 。
看着有点玄幻,本人还买的英文版,对英语没过四级的我来说就更玄幻了,不急,看代码。
public class Evalution {
public static void main(String[] args) {
Stack<String> ops = new Stack<String>();
Stack<Double> vals = new Stack<Double>();
while (!StdIn.isEmpty()) {
String s = StdIn.readString();
if (s.equals("("));
else if (s.equals("+")) ops.push(s);
else if (s.equals("-")) ops.push(s);
else if (s.equals("*")) ops.push(s);
else if (s.equals("/")) ops.push(s);
else if (s.equals("sqrt")) ops.push(s);
else if (s.equals(")")) {
String op = ops.pop();
double v = vals.pop();
if (op.equals("+")) v = vals.pop() + v;
else if (op.equals("-")) v = vals.pop() - v;
else if (op.equals("*")) v = vals.pop() * v;
else if (op.equals("/")) v = vals.pop() / v;
else if (op.equals("sqrt")) v = Math.sqrt(v);
vals.push(v);
}
else vals.push(Double.parseDouble(s));
}
StdOut.println(vals.pop());
}
}
首先,你会发现在 while
循环的条件里面乱入了一个 StdIn
,别慌,这是《算法》作者自己封装的一个工具类,这本书里面还用到其他的一些作者自己封装的工具类,在这里 (https://introcs.cs.princeton.edu/java/stdlib/) 可以下载到。
接下来,emmm...,while 循环的判断语句这里有点问题,因为这段代码运行下来,输入上面的数学表达式之后,会死循环,很惊喜,很意外,反正网上说是停止符 EOF 这些的问题,可是我是来学算法的,这些问题不管,有网友说输入上面的数学表达式之后回车,然后 Ctrl + D 就可以了,试了一下,行得通。。。
代码逻辑分析
好了终于到了最重要的环节,来分析一下这段代码的逻辑吧,搞懂了其实挺简单的,然而我花了一些时间才搞懂。。。。
嗯,每一次循环,都会读取一个操作数或者操作符,这里可能会问 sqrt 怎么处理的(其实是我自己问的),在上面就说过,操作数和操作符,用空格隔开,这样才不会出错,其实也是因为 Scanner
类的逻辑,这里不讨论。
读取到一个操作符,就是一层层的 if...else if..else... 判断了,如果是左括号就跳过本次循环,执行下一次循环,如果是其他的操作符(比如 + ,- ),就将操作符压入栈 ops ,如果不是操作符,而是操作数,就压入栈 vals 。
上的各种压栈理清楚了,是不是发现当遇见右括号的时候,画风就变了,是的,这是最重要的一步(个人认为),因为,这里就是进行计算的逻辑,嗯,到这里建议再看看前面实现 Stack 的弹栈和压栈的代码逻辑。
这一步可能图解更好,但是书上的图拍下来不清晰,有时间画了再补上来。开始分析之前,再把数学表达式搬过来:
( 1 + ( 2 + 3 ) * ( 4 * 5 ) ) )
就拿遇见的第一个右括号举例,此时,栈 ops 里面有两个元素( +,+ ),栈 vals 里面有三个元素( 1,2,3 ),遇见右括号,ops 弹栈,拿到栈顶元素操作符 +
,然后 vals 弹栈,拿到栈顶操作数 3
,此时两个栈都经过了一次弹栈, ops 和 vals 的栈顶元素分别是 +
和 2
,再看代码,又是一串 if 判断语句,并且是利用 ops 的弹栈弹出的元素做判断条件,而 操作数栈,又得弹栈一次,此时 vals 经历了两次弹栈,弹出的数据分别是 3
和 2
,然后这两个数据进行相应的算数操作,再将两个数算数操作后得出的结果压入栈 vals ,这个时候就相当于是 2 + 3
这一段现在用 5
代替。
随着 while 的再次循环,再遇见右括号,还是进行相同的弹栈压栈操作,最后栈 ops 为空,栈 vals 里面只有一个数,这个数就是最后的计算结果,这个数学表达式,就成功的得出了结果。
思考
经过上面的不专业的瞎BB,我发现这段代码好像只适合小括号里面只有两个操作数的情况,要是把 ( 2 + 3 )
改成 ( 2 + 3 +6 )
,得到的结果是 182.0 ,而不是 221 ,那么,要怎么改善一下,才能避免这种情况呢,我也是写到这里才想起来这个问题,没有代码实现,说说我现在的想法,我觉得不忽略左括号,将左括号压入 ops 栈,然后遇见右括号的时候,ops 就的弹栈直到遇见右括号,这只是个大概的思路。。。。