栈和队列

大型填坑现场,第一部分的还没写,以上。

栈和队列是很基础的数据结构,前者后进先出,后者先进先出,如下图:

下面开始将客户端和具体实现分开,这样有两个好处:一是客户端不知道实现的细节,但同时也会有很多不同实现来选择;二是实现方面也不知道客户端需求的细节,但同时很多客户端可以也重用一样的实现。接口就像把二者连接起来的桥梁。

stacks

栈的操作主要是出栈入栈,热身来一个放字符串的栈。

stack API

栈的测试代码

public static void main(String[] args) {
StackOfStrings stack = new StackOfString();
while (!StdIn.isEmpty()) {
String s = StdIn.readString();
if (s.equals("-")) StdOut.print(stack.pop());
else stack.push(s);
}
}

stack linked-list

用链表来实现栈,出栈入栈示意:

代码:

public class LinkedStackOfStrings {
private Node first = null; private class Node {
String item;
Node next;
} public boolean isEmpty() {
return first == null;
} public void push(String item) {
Node oldfirst = first;
first = new Node();
first.item = item;
first.next = oldfirst;
} public String pop() {
String item = first.item;
first = first.next;
return item;
}
}

链表实现出入栈都只要常数的时间,一直都很快,相对的会需要较多额外的空间。参考 Analysis of Algorithms 最后的内存部分。

上面算的是每个栈节点需要的空间,不包括其中的字符串,字符串开销算在客户端上。

stack array

很常见的,能用链表实现,一般还有用数组实现的版本。

详细代码在下节的变长数组里,这里说下数组游离(loitering)问题。

出栈时把不要的元素置空,垃圾回收机制才能回收不用的内存。

resizing arrays

用数组来实现栈,在栈满的时候需要自动扩大数组容量,这样才符合前面设计的 API。具体即栈满时再创建一个容量更大的数组,然后把栈里原有的元素复制过去。

要是每次创建个容量加一的数组,往栈里加入前 N 个元素,光是每次复制元素就会是平方级别(1 + 2 + ... + N ~ \(N^{2}/2\))。于是栈满的时候,我们直接把数组容量扩大两倍,这时往栈里加入前 N 个元素复制成本 2 + 4 + 8 + ... + N 和 N 成正比。

public ResizingArrayStackOfStrings() {
s = new String[1];
} public void push(String item) {
if (N == s.length) {
resize(2 * s.length);
}
s[N++] = item;
} private void resize(int capacity) {
String[] copy = new String[capacity];
for (int i = 0; i < N; i++) {
copy[i] = s[i];
}
s = copy;
}

当栈里元素数目小于数组容量时,缩减数组长度可以节省空间,缩减操作是有必要的。一样的,每次缩减一格的代价太大,但是当栈里只剩一半元素时缩减到一半也会有问题。因为前面是栈满就扩大到两倍,如果在阈值处频繁地出入栈,就会频繁地扩大缩减还有复制来复制去。于是这里等栈里只剩四分之一的时候再缩减到一半,所以数组会一直处在 25% 到 100% 满之间。

public String pop() {
String item = s[--N];
s[N] = null;
if (N > 0 && N == s.length / 4) {
resize(s.length / 2);
}
return item;
}

数组实现的栈,在时间性能方面,因为可能有数组的扩大缩减,不能保证每次都很快,但是平摊下来,出入栈操作也能在常数时间内完成。在空间方面,会比用链表实现的栈好点:

栈里有 N 个元素时,使用空间介于 ~8N 和 ~32N 比特(栈 25%~100% 满),同样的没有算上存在客户端上字符串本身。参考 Analysis of Algorithms 最后的内存部分。

综合来看,链表实现的栈保证每个操作都很快,相对的需要多一点空间;数组实现的需要的空间少点,平摊下来出入栈也算是能在常数时间内完成。所以选择哪个实现,要看具体的应用需求,比如对操作时间要求很严格就选链表,等下关键时刻碰上数组扩大缩减;要是不需要保证每次都很快,那选数组会省空间。

queues

仍然,以一个放字符串的队列为例。

queue API

queue linked-list

链表实现的队列,出入队示意:

代码:

public class LinkedQueueOfStrings {
private Node first, last; private class Node {
String item;
Node next;
} public boolean isEmpty() {
return first == null;
} public void enqueue(String item) {
Node oldlast = last;
last = new Node();
last.item = item;
last.next = null;
if (isEmpty()) first = last;
else oldlast.next = last;
} public String dequeue() {
String item = first.item;
first = first.next;
if (isEmpty()) last = null;
return item;
}
}

首尾两个节点在队列为空时要注意下。

queue array

数组实现不详述。

generics

上面我们实现了放字符串的栈和队列,要是现在需要放整数的呢,复制代码改下类型未免有点让人不太满意,泛型(generic)可以很好地解决这个问题。

把链表实现的栈改成上面那样,客户端就可以用这个栈存放任意类型的元素,只要你在声明时指定类型(Item)。另外,原始数据类型(short, int, long, float, double,, byte, boolean)需要借助对应的包装类,例如放整型的栈:Stack<Integer> s = new Stack<Integer>();

但是有一个问题,Java 不允许创建泛型数组,所以数组实现的栈里面:

s = new Item[capacity];                // can't

s = (Item[]) new Object[capacity];    // ok

下面那行可行但编译时还是会有警告,不过也没什么关系。

关于 Java 不允许创建泛型数组,可以看看链接 1链接 2 的说明。其实我不是很懂,总觉得可以用泛型数组的话,也不会写成会有问题的例子那样。

iterators

对可迭代的(iterable)对象,Java 支持更优雅的 foreach 遍历。可迭代的对象含有一个返回迭代器(iterator)的方法,迭代器里又含有方法 hasNext() 和 next()(还有 remove(),课程不建议使用)。上面的栈变成下面这样,就可以用 foreach 来遍历。

linkes-list

import java.util.Iterator;

public class Stack<Item> implements Iterable<Item> {
... public Iterator<item> iterator() {
return new ListIterator();
} private class ListIterator implements Iterator<Item> {
private Node current = first; public boolean hasNext() {
return current != null;
} public void remove() {
/* not supported */
} public Item next() {
Item item = current.item;
current = current.next;
return item;
}
}
}

array

import java.util.Iterator;

public class Stack<Item> implements Iterable<Item> {
... public Iterator<item> iterator() {
return new ReverseArrayIterator();
} private class ReverseArrayIterator implements Iterator<Item> {
private int i = N; public boolean hasNext() {
return i > 0;
} public remove() {
/* not supported */
} public Item next() {
return s[--i];
}
}
}

applications

课程建议我们在课程中不要使用 Java 里实现的栈和队列,除非你真的理解它们到底做了什么,因为商业实现的代码功能丰富,API 比较臃肿,未必像你想的那么有效率。

然后列了很多栈的应用,像网页回退,Word 中的撤销,编译器中的函数调用等等,特别介绍了算术表达式计算的双栈法。

  • 操作数:压入操作数栈。
  • 操作符:压入操作符栈。
  • 左括号:忽略。
  • 右括号:弹出两个操作数和一个操作符进行运算,结果再压入操作数栈。

代码:

public class Evaluate {
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(")")) {
String op = ops.pop();
if (op.equals("+")) vals.push(vals.pop() + vals.pop());
else if (op.equals("*")) vals.push(vals.pop() * vals.pop());
}
else vals.push(Double.parseDouble(s));
}
StdOut.println(vals.pop());
}
}

这又是 Dijkstra 发明的方法,一步步从里到外把括号里的运算换成运算结果,最后即是整个算术表达式的结果。在此基础上,可以拓展到更多的其它运算,上面的例子只有加法和乘法,操作数可以交换,要是减法和除法的话,得把先 pop() 出的减(除)数储下来,再 pop() 出被减(除)数来减(除)去前者,不能像上面那么写。更有建立优先级矩阵,可以处理不带括号的表达式等。

另外,把上面表达式的操作符放操作数后面,变成 (1 ((2 3 +) (4 5 +)*)+),Dijkstra 的双栈法也会算出同样的结果,而且这时式中的括号是冗余的,去掉也会得到正确的答案。原始的表达式操作符在操作数中间,称为中缀表达式,这里是它的后缀表达式或者叫逆波兰表示。

Stacks And Queues的更多相关文章

  1. Cracking the Coding Interview(Stacks and Queues)

    Cracking the Coding Interview(Stacks and Queues) 1.Describe how you could use a single array to impl ...

  2. 612.1.003 ALGS4 | Stacks and Queues

    Algorithm | Coursera - by Robert Sedgewick Type the code one by one! 不要拜读--只写最有感触的!而不是仅仅做一个笔记摘录员,那样毫 ...

  3. CCI_chapter 3 Stacks and Queues

    3.1Describe how you could use a single array to implement three stacks for stack 1, we will use [0, ...

  4. uva 120 stacks of flapjacks ——yhx

     Stacks of Flapjacks  Background Stacks and Queues are often considered the bread and butter of data ...

  5. UVa120 - Stacks of Flapjacks

    Time limit: 3.000 seconds限时:3.000秒 Background背景 Stacks and Queues are often considered the bread and ...

  6. Uva 120 - Stacks of Flapjacks(构造法)

    UVA - 120  Stacks of Flapjacks Time Limit: 3000MS   Memory Limit: Unknown   64bit IO Format: %lld &a ...

  7. stacks and queues--codility

    lesson 7: stacks and queues 1. Nesting 2. StoneWall 3. Brackets 4. Finsh lesson 7: stacks and queues ...

  8. Stacks of Flapjacks(栈)

     Stacks of Flapjacks  Background Stacks and Queues are often considered the bread and butter of data ...

  9. Stacks of Flapjacks

    Stacks of Flapjacks Background Stacks and Queues are often considered the bread and butter of data s ...

随机推荐

  1. IDF实验室—不难不易的js加密

    查看源代码 <html> <head> <script src="/tpl/wctf/Public/js/lib/jquery.js">< ...

  2. ES6新特性:var与let区别

    1.let的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效.var定义的变量为全局变量. 2.var在同一块可以重复定义,let不能 //正常 function () { var ...

  3. Jsp&Servlet入门级项目全程实录第2讲

    惯例广告一发,对于初学真,真的很有用www.java1234.com,去试试吧! 1.导入jquery-easyui-1.3.3包( http://www.jeasyui.com/) 2.在页面导入e ...

  4. AVPlayer 音乐播放后台播放,以及锁屏主题设置

    第一步:在appDelegate中通知app支持后台播放:在方法 - (BOOL)application:(UIApplication *)application didFinishLaunching ...

  5. 四、获取IP地址工具包

    由于getHostAddress()方法在Linux下读取hosts文件获取的是127.0.0.1 InetAddress.getLocalHost().getHostAddress() 所以这里采用 ...

  6. python学习之老男孩python全栈第九期_数据库day001知识点总结 —— MySQL操作数据库以及数据表、基本数据类型、基本增删改查、外键定义以及创建

    一. 学习SQL语句规则以及外键 1. 操作文件夹 create database db2; 创建文件夹 create database db2 default charset utf8; 创建文件夹 ...

  7. FIO测试磁盘的iops

    FIO是测试IOPS的非常好的工具,用来对硬件进行压力测试和验证,支持13种不同的I/O引擎,包括:sync,mmap, libaio, posixaio, SG v3, splice, null, ...

  8. cf1000F. One Occurrence(线段树 set)

    题意 题目链接 Sol (真后悔没打这场EDU qwq) 首先把询问离线,预处理每个数的\(pre, nxt\),同时线段树维护\(pre\)(下标是\(pre\),值是\(i\)),同时维护一下最大 ...

  9. Mybatis:使用bean传值,当传入值为Null时,提示“无效的列类型”的解决办法

    问题描述:在使用mybatis对数据库执行更新操作时,parameterType为某个具体的bean,而bean中传入的参数为null时,抛出异常如下:org.mybatis.spring.MyBat ...

  10. Android ListView的XML属性

    1.ListView的XML属性 android:divider //在列表条目之间显示的drawable或color android:dividerHeight //用来指定divider的高度 a ...