【数据结构】栈介绍 + 手写简单的ArrayDeque

34 阅读5分钟

什么是栈

栈(Stack)是一个后进先出(LIFO - Last In First Out)的线性数据结构。

与队列(先进先出)相对,栈里的元素按“最近加入的最先弹出”的规则处理。

它是相同类型元素的集合。

一个栈可以看做一个用于存储元素的列表,但限制为只能在一端添加和移除元素。

新的元素总是添加到栈顶,元素弹出的也总是栈顶元素。

image.png

栈的实现

它可以使用数组和链表实现。

但是实现栈的话更推荐数组, 下面是原因:

  1. 数组存储是连续的,不需要使用后继指针指向下一个存储节点
  2. 减少垃圾回收:
  • 数组实现时只需要分配一次空间。
  • 链表需要每次删除都会回收栈顶元素
  1. 内存消耗更少:
  • 数组只需要一个指针来跟踪栈顶。
  • 链表需要额外保存头节点和tail节点。

综上: 使用数组可能会更好一点

栈的基本操作

  • push(): 将元素添加到栈顶。
  • pop(): 移除栈顶的元素,返回栈顶的值。
  • peek():返回栈顶元素, 仅仅只是查看,不会移除栈顶元素。
  • isEmpty(): 判断栈是否为空。
  • size(): 返回栈的元素个数。

栈的常见用途包括:

希望这能解释清楚什么是栈?它的特点、方法以及常见用途!

栈的使用场景

  • 函数的调用栈(典型的比如递归操作)
  • 回退操作(如网页的后退按钮)
  • 查看最近消费记录,越晚的越在前面

Java中栈的使用

Java里面有一个Vector接口,Java的Stack实现了这个类可以用于实现栈,但是呢,但是看百度说这个类很老了,Satck很多方法都是加了锁的(而且直接加在了方法上),因此执行效率肯定很低。

ArrayDeque优点:

  • 更高效的扩容方式,通过判断当前数组大小来扩充容量。
  • 提供了更多的双端队列操作。
  • 使用数组实现,存储占用小

总的来说,就是Stack相比栈的另一个栈实现ArrayDeque显得粗糙,因此,Java官方也推荐使用ArrayDeque作为栈的实现。

它的扩容不是一般的像ArrayList那样直接扩容1.5倍,而是根据当前的数组大小来进行扩容的,

  • 容量小于64时,容量扩大一倍。
  • 容量大于64时,容量增加50%。

手写ArrayDeque

我们手写的这个ArrayDeque主要是用来实现栈的,所以用不到写完所有方法。

我们使用ArrayDeque来实现的话,应该考虑怎么将其作为栈,首先,它虽然是一个队列,但是也可以作为栈。

队列的头部数据是最先添加的,因此弹出也从头部,这样就符合了栈的后进先出。

ArrayDeque的数组实现其实是一个循环数组,下面写道具体方法时再介绍。

写完之后的最终结构是: image.png

从上到下依次是:

  • 存储数组的Object数组
  • 头指针
  • 尾指针
  • 构造函数
  • 入栈方法
  • 出栈方法
  • 判断为空
  • 判断是否已满

你可能发现我们并没有维持一个变量用于存储当前数组的有效数字大小size,因为我们使用循环数组,容量是否已满不需要使用size,根据头尾指针之间的关系就能计算出size,我这里没有书写。

构造函数

FakeArrayDeque(){
    // 实际能保存16个数据
    elements = new Object[16+1];
}

我虽然开的是17的数组,但是还有一个空间是不会存储数据的,它用来区分数组为空和数组满。

当head与tail处于同一地方,表示数组为空。

当head-1==tail,表示数组已满。

入栈操作

public void push(E e){
    if(--head < 0){
        head = elements.length-1;
    }
    elements[head] = e;
    if(head-1 == tail){
        // 进行扩容
    }
}

不管怎样,加入数据,头指针一定会往左移动一位,当然可能超过数组,超过数组就将其放到数组末尾。

最后,判断数组是否已满,条件为tail == head - 1.这个时候表示数组已满

image.png

出栈操作

public E poll(){
    Object result = elements[head];
    if(++head >= elements.length){
        head = 0;
    }
    return (E)result;
}

不管怎样,先取出头指针指向的数据,然后头指针右移,当头指针与尾指针重合,表示数组为空。

image.png

如图所示,此时数组为空。

判断空和满

public boolean isEmpty(){
    return tail == head;
}
public boolean isFull(){
    return tail==head-1;
}

根据头尾指针位置来判断。

测试函数

public static void main(String[] args) {
    FakeArrayDeque<String> list = new FakeArrayDeque<>();
    for (int i = 0; i < 16; i++) {
        list.push("nmd" + (i+1));
    }
    System.out.println("是否已经装满:"+list.isFull());
    for (int i = 0; i < 16; i++) {
        System.out.println(list.poll());
    }
    System.out.println("是否已经为空"+list.isEmpty());

}

输出

是否已经装满:true
nmd16
nmd15
nmd14
nmd13
nmd12
nmd11
nmd10
nmd9
nmd8
nmd7
nmd6
nmd5
nmd4
nmd3
nmd2
nmd1
是否已经为空true.

差不多实现了入队与出队的操作。

扩容的操作我没有写,我们实现栈的话,满的情况只有一个

image.png

这时候扩容就可以:

System.arraycopy(elements, head, newElements, 0, elements.length - head);
System.arraycopy(elements, 0, newElements, elements.length - head, tail);
elements = newElements;
head = 0;
tail = size;
  1. 将原数组头部到末尾的数据复制到新数组的0到有效数据长度处
  2. 将原数组索引0位置到尾部的数据复制到新数组的空闲区域。
  3. 这样得到的新数组又变成一个头在索引0,尾部指向下一个要进入的元素索引处。

image.png

总结

  1. 栈是一个线性的数据结构,有着后进先出的特点
  2. 栈可以使用链表和数组实现,数组更快
  3. 栈在需要后进先出的场景使用,比如递归,页面回退
  4. Java中Stack实现了栈,但不推荐使用,推荐使用ArrayDeque
  5. Java中ArrayDeque实现了栈,数据结构为循环数组
  6. 手写ArrayDeque要注意循环数组判断空和满的情况,数组留一个空用来判断,扩容的话就是从头复制到旧数组尾,然后从旧数组开始复制到tail。