Stacks and Queue

一、基本概念

image.png

  • 堆栈:检测最后加入的对象。后进先出,插入元素又叫入栈(push),去掉最近加入的元素又叫出栈(pop)。
  • 队列:检测最先加入的对象。先进先出,插入元素又叫入队(enqueue),去掉最近加入的元素又叫出队(dequeue)。

二、stacks:基于链表实现

指针始终指向链表的第一个节点,在最前方进行插入和删除操作
image.png

以字符串为存储对象的 stacks 举例

stack 需要实现的API
image.png

内部类

private class Node 
{ 
    String item;
    Node next;
}

pop出栈操作
1、 保存头节点中存储的对象

String item = first.item;

2、改变指针指向下一个节点(原本的头结点会被垃圾回收器回收)
image.png

first = first.next;

3、 返回1中保存下来的对象

return item;

push 入栈操作

1、创建一个新指针指向当前的头结点
image.png

Node oldfirst = first

2、创建一个新的头节点,并将first指针指向该节点
image.png

first = new Node();

3、给新节点设置实例变量
image.png

first.item = "not"
first.next = oldfirst

三、stacks:基于链表实现的性能表现

  1. 时间: 由于没有for循环,每个方法都只有个别指令,因此是常数项的复杂度
  2. 空间:节点数为N,则内存占用 ~40N 字节:

image.png

内存占用项占用空间(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)
image.png

六、重置数组容量的实现

每次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中的代码)。
image.png
虽然有些push操作会进行多次数组的运算,但从宏观上来看,总的开销是正比于3N的访问次数,这叫做平摊分析(amortized analysis)

  1. 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;
}

ahtlzj
1 声望1 粉丝

人工智能成长ing, 关注我就是关注一个潜力股