数据结构之栈与Java中的Stack、Deque

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_45608306/article/details/101120948

1.栈的定义

栈是一种特殊的线性表,栈的数据元素以及数据元素之间的逻辑关系和线性表完全相同,其差别是线性表允许在任意位置进行插入和删除操作,而栈只允许固定一端进行插入和删除操作。
栈中允许插入和删除的一端称为栈顶,另一端称为栈底。栈的插入操作通常称为进栈或入栈,栈的删除操作通常称为退栈或出栈。
由于每次入栈的元素都放在原栈顶元素之前成为新的栈顶元素,每次出栈的数据元素都是原栈顶元素,这样导致最后进入栈的数据元素总是最先退出栈,因此,栈也称为"后进先出表"。
栈

2.栈类型、结构及其特点

2.1 顺序栈
顺序存储结构的栈称为顺序栈,故通常来说,顺序栈基于数组实现。顺序栈与顺序表的数据成员类似,值得注意的是顺序栈的入栈和出栈操作只能在栈顶进行。
由于插入删除操作均在栈顶,故通常将数组数据元素增长的那端设置为栈顶,而低位置数据区域设置为栈底。
顺序栈其结构具有如下类似形状:
顺序栈
maxSize表示栈的最大数据元素个数,top表示当前栈顶下标。
顺序栈的缺点是很容易造成栈溢出,即栈中存储元素达到上限。故可考虑实现扩容机制,然而扩容机制会带来较大开销,因此,栈通常采用链式的实现。
2.2 链式栈
由于栈的特殊性,通常不需要对栈进行遍历,也不需要知道栈的某个前驱或后继元素,并且只在一头插入删除,因此将单链表作为栈的链式存储结构。
考虑插入和删除操作:
1)插入和删除操作只在栈顶进行,因而将靠近头的一方设置为栈顶可使得插入的时间复杂度最小,为 O ( 1 ) O(1) ,如若设置末尾为栈顶,则每次插入和删除操作均需遍历链表,时间复杂度为 O ( N ) O(N)
2)插入和删除操作只在栈顶进行,也即在链表头的部分进行,因而不需要设置头结点也不会使插入删除操作有多样性,同样可以简便实现,因而链式栈不设置头结点。
链式栈
2.3
2.4

3.顺序栈的基本功能实现

3.1 由于顺序栈实现比较简单,不做分析
首先了解,栈基本操作有:出栈,入栈,取栈顶元素,判空等。

public class myArrayStack {
    private static final int defaultSize = 10;  //栈的默认大小
    int top;         //栈顶标记
    Object[] stack;    //栈元素存储的数组
    int maxSize;    //栈的最大容量
    
    public myArrayStack(){
        maxSize = defaultSize;
        top = 0;
        stack = new Object[defaultSize];
    }

    public myArrayStack(int size){ //初始化栈相关信息
        maxSize = size; 
        top = 0;  
        stack = new Object[size];
    }
    
    public Object getTop() throws Exception{
        if (top == 0){   //判空
            throw new Exception("stack is empty");
        }
        return stack[top-1];   //返回栈顶元素
    }
    
    public Object pop() throws Exception{
        if (top == 0){
            throw new Exception("stack is empty");
        }
        top--;    //移动栈标记
        return stack[top];   //返回本次操作删除的元素
    }
    
    public void push(Object obj) throws Exception{
        if (top == maxSize){  //判满
            throw new Exception("stack is full");
        }
        stack[top] = obj;   //插入元素
        top++;      //移动栈标记
    }
    
    public boolean isEmpty(){
        return top == 0;
    }
}

4.链式栈的基本功能实现

public class myLinkedStack {
    Node head;  //头节点,栈顶
    int size;   //栈存储的数据量

    private static class Node{
        Object data;
        Node next;

        Node(Object obj){
            data = obj;
        }
    }

    public myLinkedStack(){
        head = null;
        size = 0;
    }

    public void push(Object obj){
        Node p = new Node(obj);
        p.next = head; //新结点的指针域指向原栈顶
        head = p;    //将新结点设置为栈顶
        size++;
    }

    public Object pop() throws Exception{
        if (head == null){
            throw new Exception("stack is empty");
        }
        Object obj = head.data;  //保存栈顶数据元素
        head = head.next;   //将原栈顶的后方元素设置为栈顶
        size--;
        return obj;
    }

    public Object getTop() throws Exception{
        if (head == null){
            throw new Exception("stack is empty");
        }
        return head.data;
    }

    public boolean isEmpty(){
        return head == null;
    }
}

5.性能分析

5.1 顺序表入栈
由于入栈操作在数组尾部进行,故入栈操作不需要移动元素,由于栈顶标记的存在,搜索也在 O ( 1 ) O(1) 时间完成,故顺序表入栈时间复杂度为 O ( 1 ) O(1)
5.2 顺序表出栈
同5.1,顺序表出栈时间复杂度为 O ( 1 ) O(1)
5.3 链式表入栈
链式表栈顶在链头,故无需搜索,链式表的插入和删除不需要移动元素,因而,链式表入栈时间复杂度为 O ( 1 ) O(1)
5.4 链式表出栈
同5.3,链式表出栈时间复杂度为 O ( 1 ) O(1)
5.5 综合情况
由于顺序栈存在栈满的情况,若想将已满的顺序栈复制入一个更大的空栈则需要花费大量的时间,因而,在不确定有多大规模的数据需要存入栈中时,采用链式表作为存储结构。
另一方面来说,链式栈存储指针域需要花费更多空间,因而在得知入栈数据规模又希望节省存储空间时,采用链式栈。

Stack

1.Stack介绍

官方介绍:Stack类表示对象的后进先出(LIFO)堆栈。 它通过五个操作扩展了Vector类,这些操作允许将Vector视为堆栈。这五个操作分别为:入栈,出栈,判空,查看栈顶元素,计算某元素离栈顶的位置。

2.Stack部分源码分析

2.1 入栈

    public E push(E var1) {
        this.addElement(var1);     //直接调用Vector类中的添加元素操作
        return var1;
    }

addElement调用的Vector中的方法

    public synchronized void addElement(E var1) {
        ++this.modCount;       //修改次数计数
        this.add(var1, this.elementData, this.elementCount); //调用add方法
    }
    private void add(E var1, Object[] var2, int var3) {
        if (var3 == var2.length) {   //数组满判定
            var2 = this.grow();     //数组扩容
        }
        var2[var3] = var1;  //添加新元素
        this.elementCount = var3 + 1;   //数据元素计数
    }

对于其扩容操作详情,其扩容为一个固定大小,可见数据结构之顺序表与Java中的ArrayList、Vector
2.2 出栈

    public synchronized E pop() {
        int var2 = this.size();   //获取栈顶下标
        Object var1 = this.peek();    //保存栈顶元素
        this.removeElementAt(var2 - 1);   //调用Vector中的删除方法
        return var1;
    }

removeEkementAt方法

    public synchronized void removeElementAt(int var1) {
        if (var1 >= this.elementCount) {  //越界检测
            throw new ArrayIndexOutOfBoundsException(var1 + " >= " + this.elementCount);
        } else if (var1 < 0) {   //越界检测
            throw new ArrayIndexOutOfBoundsException(var1);
        } else {
            int var2 = this.elementCount - var1 - 1;    //计算删除元素后驱的元素数量
            if (var2 > 0) {
                System.arraycopy(this.elementData, var1 + 1, this.elementData, var1, var2);   //后面所有数据前移
            }

            ++this.modCount;
            --this.elementCount;   //修改元素数量
            this.elementData[this.elementCount] = null;  //将末位置空
        }
    }

2.3 查看栈顶元素

    public synchronized E peek() {
        int var1 = this.size();   //获取栈顶下标
        if (var1 == 0) {
            throw new EmptyStackException();  //判空
        } else {
            return this.elementAt(var1 - 1);  //返回栈顶元素
        }
    }

2.4 计算某元素离栈顶位置

    public synchronized int search(Object var1) {
        int var2 = this.lastIndexOf(var1);  //调用Vector中的lastIndexOf方法,返回var1的索引
        return var2 >= 0 ? this.size() - var2 : -1;   //返回离栈顶的距离
    }

lastIndexOf方法

    public synchronized int lastIndexOf(Object var1) {
        return this.lastIndexOf(var1, this.elementCount - 1);//调用它的重载方法,添加参数:元素数量-1,也即最后一个元素下标
    }

    public synchronized int lastIndexOf(Object var1, int var2) {
        if (var2 >= this.elementCount) {
            throw new IndexOutOfBoundsException(var2 + " >= " + this.elementCount);
        } else {
            int var3;
            if (var1 == null) {  //查找的是空元素
                for(var3 = var2; var3 >= 0; --var3) {
                    if (this.elementData[var3] == null) {  //往前找,到第一次发现新元素
                        return var3;
                    }
                }
            } else {
                for(var3 = var2; var3 >= 0; --var3) {    //从后往前匹配
                    if (var1.equals(this.elementData[var3])) {
                        return var3;    //返回查找到的元素的索引
                    }
                }
            }

            return -1;
        }
    }

3.为什么Stack不建议使用

集合框架引进的时候,Vector加入集合大家族,改成实现List接口,需要实现List接口中定义的一些方法,但是出于兼容考虑,又不能删除老的方法,所以出现了一些功能冗余的旧方法;现在已经被ArrayList取代,基本很少使用。而Stack又继承自Vector,这表明Stack是线程安全的顺序栈。
然而在线程安全必然带来开销,这是很多时候我们不希望看到的;另一方面,基于数组实现的Stack在频繁入栈操作时会进行扩容,,而扩容会带来操作效率的降低。
官方称:Deque接口及其实现提供了一组更完整和一致的LIFO堆栈操作,应优先于Stack使用。
关于线程安全,应该使用java.util.concurrent包下的类。

Deque

1.使用Deque代替Stack

Deque是支持在两端插入和删除元素的线性集合。可将其称为"双端队列",双端队列也可以用作LIFO(后进先出)堆栈。
作为栈使用时,可使用

void push​(E e);
E pop();
E peek();

三个方法
Deuqe接口的实现有ArrayDeque(数组实现)和LinkedList(链式实现)

2.ArrayDeque部分源码分析

2.1 成员

    transient Object[] elements;    //存储数据的数组
    transient int head;       //头部标记
    transient int tail;       //尾部标记

2.1 构造函数
ArrayDeque提供三个构造方法,1)默认构造方法数组大小为16,2)给定整数构建双端队列,3)根据给出的集合及其元素构建双端队列

    public ArrayDeque() {
        this.elements = new Object[16];
    }

    public ArrayDeque(int var1) {
        this.elements = new Object[var1 < 1 ? 1 : (var1 == 2147483647 ? 2147483647 : var1 + 1)];
    }

    public ArrayDeque(Collection<? extends E> var1) {
        this(var1.size());   //集合大小
        this.copyElements(var1);   //复制元素
    }

2.2 入栈

    public void push(E var1) {
        this.addFirst(var1);  //调用在首部插入的方法
    }
    public void addFirst(E var1) {
        if (var1 == null) {
            throw new NullPointerException();   
        } else {
            Object[] var2 = this.elements;    
            var2[this.head = dec(this.head, var2.length)] = var1;   //dec方法是:让第一个参数标识的下标往前挪动一个单位(带循环的挪动),因而此方法是,将新数据添加至头标记的前一个位置
            if (this.head == this.tail) {   //如果头标记和尾标记碰头,也即数组已存满,则扩容
                this.grow(1);    //扩容方法见2.5
            }
        }
    }

2.3 出栈

    public E pop() {
        return this.removeFirst();   //调用首部删除方法
    }
    public E removeFirst() {
        Object var1 = this.pollFirst(); 
        if (var1 == null) {
            throw new NoSuchElementException();
        } else {
            return var1;
        }
    }
    public E pollFirst() {
        Object[] var1;
        int var2;
        Object var3 = elementAt(var1 = this.elements, var2 = this.head);   //获取头标记指向元素
        if (var3 != null) {   //若非空
            var1[var2] = null;   //将其置空
            this.head = inc(var2, var1.length);   //往后移动头标记(循环移动)
        }

        return var3;
    }

2.4 查看栈顶元素

    public E peek() {
        return this.peekFirst();
    }
    public E peekFirst() {
        return elementAt(this.elements, this.head);  //定位头标记元素并返回
    }

2.5 扩容
其基本机制可看作,grow给出一个"期望增长大小"。
之后由原数组大小计算得一个 “可选增长大小”,其公式为 l e n = a r r a y . l e n g t h < 64 ? a r r a y . l e n g t h + 2 : a r r a y . l e n g t h / 2 len = array.length < 64?array.length +2:array.length/2
之后比出 “期望增长大小” 和 “可选增长大小” 中更大的那个 m a x max ,"新容量"为 a r r a y . l e n g t h + m a x array.length+max

    private void grow(int var1) {
        int var2 = this.elements.length;  //数组长度
        int var4 = var2 < 64 ? var2 + 2 : var2 >> 1;   //原数组长度小于64则为原数组长度+2;否则减半
        int var3;
        if (var4 < var1 || (var3 = var2 + var4) - 2147483639 > 0) {
            var3 = this.newCapacity(var1, var4);   //新数组大小可看作:var1和var4中更大的那个+原数组大小
            //若var4小于给出的期望增长大小,则新大小为原数组大小+var4
        }

        Object[] var5 = this.elements = Arrays.copyOf(this.elements, var3);   //拷贝至新数组
        if (this.tail < this.head || this.tail == this.head && var5[this.head] != null) {  //调整数据位置
            int var6 = var3 - var2;
            System.arraycopy(var5, this.head, var5, this.head + var6, var2 - this.head);
            int var7 = this.head;

            for(int var8 = this.head += var6; var7 < var8; ++var7) {
                var5[var7] = null;
            }
        }

    }

    private int newCapacity(int var1, int var2) {
        int var3 = this.elements.length;
        int var4;
        if ((var4 = var3 + var1) - 2147483639 > 0) {   //超过最大整数
            if (var4 < 0) {
                throw new IllegalStateException("Sorry, deque too big");
            } else {
                return 2147483647;
            }
        } else if (var1 > var2) {  //若参数1>参数2,则新容量为:原数组大小+参数1
            return var4;
        } else {   //若参数1<参数2,则新容量为:原数组大小+参数2
            return var3 + var2 - 2147483639 < 0 ? var3 + var2 : 2147483639;
        }
    }

3.再谈LinkedList

3.1 入栈
入栈操作调用了addfirst,addfirst又调用了linkfirst
3.2 出栈
出栈操作调用了removefirst,removefirst又调用了unlinkFirst
3.3 查看栈顶元素
查看栈顶元素搜索头部元素
3.4 linkFirst和unlinkFirst
linkFirst和unlinkFirst在数据结构之链表与Java中的LinkedList中有详细介绍。

总结

1.栈为"先进后出表",通常用来临时存储接下来可能还会马上用到的元素。
2.顺序栈和链式栈的出入栈操作没有效率上的差别,而顺序栈固定大小,扩容操作带来较大开销,故通常考虑链式栈而不考虑顺序栈。
3.使用ArrayDeque(顺序)或LinkedList(链式)来代替Stack,需要线程安全时使用ConcurrentLinkedDeque

猜你喜欢

转载自blog.csdn.net/qq_45608306/article/details/101120948