栈和队列

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

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

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

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. 2013年8月12日Python的5个最有价值问题

    问:Python怎么在字典里删除值但保留相应的键 答: 假设3都在值里,而非键>>> for v in D1.values():...     if 3 in v:...       ...

  2. Linq学习教程

    http://www.cnblogs.com/foundation/archive/2009/01/05/1369371.html

  3. Wirshark 显示过滤器

    二层显示过滤: eth.addr==<MAC Address>:只显示具有指定mac地址 eth.src==<MAC Address>:只显示具有指定源MAC地址的数据帧 et ...

  4. Action Bar

    1.显示隐藏Action Bar 1.配置上 在application 上的 android:theme="@style/AppTheme"全局配置ActionBar在某个acti ...

  5. leetcode实践:找出两个有序数组的中位数

    题目 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2. 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n)). 代码实现 package com. ...

  6. 资源在Windows编程中的应用

    学习目的 掌握菜单和对话框资源的创建和使用. 编写程序: 设计一个窗口应用程序, 其中有一个VC菜单, 该菜单下有"显示", "隐藏", "退出&qu ...

  7. idea创建maven的web工程

    然后一路点next 接下去添加tomcat 成功 控制台出现乱码的话 输入:-Dfile.encoding=UTF-8 控制台乱码解决

  8. webpack2引入bootstrap的坑

    在webpack官网教程的代码分离-css章节中,给出的例子是这样的. //安装 ExtractTextWebpackPlugin 如下 npm install --save-dev extract- ...

  9. LOJ#6035. 「雅礼集训 2017 Day4」洗衣服

    传送门 先处理出每一件衣服最早什么时候洗完,堆+贪心即可 然后同样处理出每件衣服最早什么时候烘干 然后倒序相加取最大值 # include <bits/stdc++.h> using na ...

  10. BZOJ4566: [Haoi2016]找相同字符(后缀自动机)

    题意 题目链接 Sol 直接在SAM上乱搞 枚举前缀,用SAM统计可以匹配的后缀,具体在匹配的时候维护和当前节点能匹配的最大值 然后再把parent树上的点的贡献也统计上,这部分可以爆跳parent树 ...