Stack and Queue 栈和队列
0.前言
本文是对数据类型的学习笔记,将会从接口(interface),实现(implement), 客户(client)等三个角度分模块来组织文章。
接口:对数据类型、基本操作的描述(API)
实现:实现API的基本操作的实际代码
客户:在程序里调用API所定义的基本操作
1.Stack 栈
栈最基本的特点就是后进先出(LIFO, last in first out)
- 栈的API
//栈的API
public class Stack<Item>{
//<Item>表示将数据类型泛化,此栈元素可接受任意数据类型
public Stack();
public void push(Item item);
public Item pop();
public boolean isEmpty();
public int size;
//其他
}
- 栈的实现
栈的实现有两个(基本)方式,分别是链表(linked-list)和可调整数组(resizing array)
链表
入栈和出栈表示
入栈
出栈
代码实现
public class Stack<Item>{
private Node first = null;
//因为构造时不做任何操作,所以不写构造方法
//内部类定义链表节点,注意访问修饰符private
private class Node{
Item item;
Node next;
}
public boolean isEmpty(){
return first == null;
}
public void push(Item item){
Node oldfirst = first;
first = new Node();
first.item = item;
first.next = oldfirst;
}
public Item pop(){
Item item = first.item;
first = first.next;
return item;
}
}
时间性能
出栈和入栈的操作都是顺序语句,常数级(constant)
空间复杂度
在针对String字符串时分析:Node节点对象占用40字节,故所有空间~40N字节,如图
可调整数组
入栈和出栈表示
push: 往数组的的末尾添加item
pop: 在数组的末尾删除item
难处:由于无法提前知道栈的容量,导致在定义数组时无法确定其大小。初始的数组容量过大会浪费空间,容量过小会导致运行过程中溢出。因此,使用可调整的数组来将解决可以难题。
关键:当数组容量满(full)的时候,将数组大小翻倍(double size),当数据容量为大小的四分之一(one-quarter full)的时候,将数组大小减半(halve size)。
代码实现
public class Stack<Item>{
private Item[] s;
private int N;
public Stack(){
//由于java不支持泛型数组,所以这里需要强转
s = (Item[])new Object[1];
N = 0;
}
public boolean isEmpty(){
return N > 0;
}
public void push(Item item){
if (N == s.length) resize(2 * s.length);
s[N++] = item;
}
public Item pop(){
Item item = s[--N];
s[N] = null;
if (N > 0 && N == s.length/4) resize(s.length/2);
return item;
}
private void resize(int capacity){
Item[] copy = (Item[])new Object[capacity];
for (int i = 0; i < N; i++)
copy[i] = s[i];
s = copy;
}
}
时间性能
由于在入栈和出栈的过程中需要调整数组大小(此过程需要复制完整数组),若调整过于频繁,必定会耗时严重。因此时间性能分析存在最好、最坏和平摊情况。
*平摊分析:在最坏情况下,每次操作所需的平均时间。
空间复杂度
在针对String字符串时分析:在full的时候需要~8N,在1/4 full的时候需要~32N(暂时未能解释)
对比
-
链表每一个操作的耗时都是常数级,但比较占用空间(存字符串要~40N)
-
可调整数组的每一个操作的摊分时间也是常数级,但可能会遇到最坏情况而影响性能,其空间占用较小
2.Queue队列
队列最基本的特点就是先进先出(FIFO, first in first out)
- 队列的API
//队列的API
public class Queue<Item>{
//Item表示将数据类型泛化,此队列元素可接受任意数据类型
public Queue();
public void enqueue(Item item);
public Item dequeue();
public boolean isEmpty();
public int size;
//其他
}
- 队列的实现
栈的实现同样有两个(基本)方式,分别是链表(linked-list)和可调整数组(resizing array)
链表
入队和出队表示
入队
出队
代码实现
public class Queue<Item>{
private Node first = null;
private Node last = null;
//因为构造时不做任何操作,所以不写构造方法
//内部类定义链表节点,注意访问修饰符private
private class Node{
Item item;
Node next;
}
public boolean isEmpty(){
return first == null;
}
public void enqueue(Item item){
Node oldlast = last;
last = new Node();
first.item = item;
first.next = null;
if (isEmpty()) first = last;
else oldlast.next = last;
}
public Item pop(){
Item item = first.item;
first = first.next;
if (isEmpty()) last = null;
return item;
}
}
时间性能
出队和入队的操作都是顺序语句,常数级(constant)
空间复杂度
在针对String字符串时分析:Node节点对象占用40字节,故所有空间~40N字节
可调整数组
入队和出队表示
push: 往数组的的末尾(tail)添加item
pop: 在数组的首部(head)删除item
关键:调整首部和末尾的指针,以及调整数组的容量
代码实现
public class Queue<Item>{
//这里使用循环队列来实现,必须固定长度。
//这里暂时没有实现调整数组容量的操作
private final static int N =10 ;
private Item[] s;
private int tail;
private int head;
public Stack(){
//由于java不支持泛型数组,所以这里需要强转
s = (Item[])new Object[1];
tail = 0;
head = 0;
}
public boolean isEmpty(){
return head == tail;
}
public void enqueue(Item item){
if ((tail + 1) % N == head ){
//溢出,不允许入队
return ;
}
s[tail] = item;
tail = (tail + 1) % N;
}
public Item dequeue(){
if(isEmpty()){
//空队列,不允许出队
return;
}
Item item = s[head];
head = (head + 1) % N;
return item;
}
}
时间性能
循环队列由于不涉及调整数组容量的操作,所以入队和出队操作都是顺序语句,常数级(constant)
空间复杂度
空间占用就是数组的容量,拿String字符串来分析,就是~8N
3.泛型
前面的代码已经涉及泛型,使用泛型的目的就是为了能够适配所有的数据类型。
在使用可调整数组实现栈的时候提到:由于java不支持定义泛型数组(其他程序语言也不支持),所以在定义泛型数组的时候使用强制转型
//强转
Item[] copy = (Item[])new Object[capacity];
虽然使用强转是一种糟糕的编程习惯,但是此场景下只能通过该方法解决。
另外,学习泛型还需要学习java数据类型的包装类(wrapper type)和自动装箱子机制(autoboxing)。
//int类型的包装类是Integer
//push操作中会将int自动转成Integer,称为自动装箱
Stack<Integer> s = new Stack<Integer>();
s.push(17);
4.迭代器
使用迭代器的好处就是可以方便地遍历栈(或者队列)的元素,例如使用foreach方法来遍历。
类实现迭代器的基本方法如下:
public class Stack<Item> implements Iterable<Item>{
//省略了其他属性和方法
//重写iterator()方法,返回自定义的iterator对象
public Iterator<Item> iterator() {
return new ListIterator();
}
//自定义iterator类
private class ListIterator implements Iterator<Item>{
private Node current = first;
//必须重写next()he hasnext()方法
public boolean hasNext() {
return current != null;
}
public void remove() { /* not supported */ }
public Item next(){
Item item = current.item;
current = current.next;
return item;
}
}
}
5.应用
-
java本身就提供了非常丰富的集合类(如List、Stack等),但是其提供的操作都是非常冗余且低性能的。如果对性能有追求的最好自己封装集合类。
-
函数的调用就是对栈的而一种应用。由于函数的调用,程序流程进入函数内部,其当前环境和变量将全部推入栈,直到调用完毕返回结果时,才出栈。注意多级调用和递归调用。
-
对于表达式的运算(如下图),使用双栈来完成:1)遇到左括号不操;2)遇到数字push到value satck;3)遇到操作符push到operator stack;4)遇到有括号从value satck pop两个数字,再从operator stack pop一个操作符,最后将计算结果push 到value stack。