Stacks and Queue
一、基本概念
- 堆栈:检测最后加入的对象。后进先出,插入元素又叫入栈(push),去掉最近加入的元素又叫出栈(pop)。
- 队列:检测最先加入的对象。先进先出,插入元素又叫入队(enqueue),去掉最近加入的元素又叫出队(dequeue)。
二、stacks:基于链表实现
指针始终指向链表的第一个节点,在最前方进行插入和删除操作
以字符串为存储对象的 stacks 举例
stack 需要实现的API
内部类
private class Node
{
String item;
Node next;
}
pop出栈操作
1、 保存头节点中存储的对象
String item = first.item;
2、改变指针指向下一个节点(原本的头结点会被垃圾回收器回收)
first = first.next;
3、 返回1中保存下来的对象
return item;
push 入栈操作
1、创建一个新指针指向当前的头结点
Node oldfirst = first
2、创建一个新的头节点,并将first指针指向该节点
first = new Node();
3、给新节点设置实例变量
first.item = "not"
first.next = oldfirst
三、stacks:基于链表实现的性能表现
- 时间: 由于没有for循环,每个方法都只有个别指令,因此是常数项的复杂度
- 空间:节点数为N,则内存占用 ~40N 字节:
内存占用项 | 占用空间(bytes) |
---|---|
对象头部 | 16 |
内部对象额外的开销 | 8 |
指向字符串的指针 | 8 |
指向下一个节点的指针 | 8 |
四、stacks:基于数组实现
基于数组实现的缺陷
数组是有容量的,存储的数量可能超过容量,需要对其进行处理。
public class FixedCapacityStackOfStrings
{
private String[] s;
private int N = 0;
//这里假设提前设置好数组大小,之后会解决这个问题
public FixedCapacityStackOfStrings(int capacity)
{ s = new String[capacity]; }
public boolean isEmpty()
{ return N == 0; }
public void push(String item)
// 使用当前索引插入数组,然后递增N
{ s[N++] = item; }
public String pop()
// 递减N,然后使用索引访问数组元素
{ return s[--N]; } }
五、对于stacks数组实现更多的思考
如何处理数组的上溢和下溢
下溢(Underflow):从空堆栈中进行pop操作会抛出异常
上溢(Overflow):扩容数组容量(resizing array)
空值: 允许空值插入
对象游离(loitering)
六、重置数组容量的实现
每次push数组容量递增,pop数组容量递减(<span style="color:red">×</span>)
如果采取每次push增大数组1个容量,pop减少数组1个容量的方式,每次resize都需要拷贝数组中的对象到新的数组中,那么插入N个元素花费的时间正比于 1+2+3+...+N ~ N^2/2,在数据量大的时候无法接受。
push —— 反复翻倍(<span style="color:green">√</span>)
每当数组达到容量上限,创建一个两倍大小的数组,然后将旧数组的元素拷贝到新数组
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;
}
此时插入N个元素花费的时间上正比于 <font color="green">N</font> + <font color="red">(2 + 4 + 8 + ... + N)</font> ~ 3N
其中 <font color="green">N</font> 是 N 个元素在进行 push 操作时数组的访问次数,而 <font color="red">2 + 4 + 8 + ... + N</font> 是达到数组上限后,拷贝数组过程中对旧数组的访问次数
下图可以看到每遇到2的幂,需要进行多次数组的访问(数组扩容,拷贝原数组容量数量的对象到新数组),其余时间数组的访问次数仅为常数次(具体数组访问次数取决于push中的代码)。
虽然有些push操作会进行多次数组的运算,但从宏观上来看,总的开销是正比于3N的访问次数,这叫做平摊分析(amortized analysis)
- pop —— 对象为容量的 1/4 大小时容量减半 (<span style="color:green">√</span>)
不在 1/2 大小减半的原因是会出现抖动(shrashing),push元素时数组满了翻倍,随即pop时数组又少于容量的1/2于是减半,随即push翻倍、pop减半、push翻倍、pop减半...于是每次操作都需要花费正比于数据量 N 的时间。
public String pop() {
String item = s[--N];
s[N] = null;
if (N > 0 && N == s.length/4) resize(s.length/2);
return item;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。