美文网首页
手写一个Stack(栈)的集合类(链表实现+数组实现)

手写一个Stack(栈)的集合类(链表实现+数组实现)

作者: 生不悔改 | 来源:发表于2022-03-08 13:38 被阅读0次

    一、Stack栈集合的原理

    栈是程序中一种数据结构,它的出入口是同一个,所以,出栈操作,和入栈操作,都是从同一个口子。看一下示意图。

    栈的示意图.png
    图中,类似杯子的容器其实就是这种容器集合结构。如图所示,将元素A,B,C,D按照顺序加入到栈中,先加入的A元素反而在容器栈的最底端。这时候如果我们需要取元素,那么只能从栈的最上面元素开始取,而D元素,作为栈最上面的元素,却是最后一个加入的。所以栈这种数据结构有一个重要的特性:先进后出(First In Last Out),简称FILO。

    二、栈的常见操作:

    1.压栈

    将元素放进栈中,这个操作叫压栈。

    1.弹栈

    将元素从栈中取出来,这个操作叫弹栈,也可以叫出栈。

    三、实现Stack数据结构的思路

    1.链表实现思路

    这里的链表我们采用单向链表,双向的链表也可以实现,只是相对来说会比单向链表复杂一点。我们要理解栈的特性:先进后出。所以我们可以让每次新加入的元素,放在后加入元素的前面,例如:0,加入一个元素1,变成0->1;如果再加入一个元素2,此时变成了0->2->1;再加入3,变成0->3->2->1。由于我们的链表只能从头节点开始遍历,这样我们在遍历链表的时候,就可以完成栈这个先进后出的特性了。

    a.链表实现代码
    /**
     * @author: 
     * @date: 2022/3/8 09:45
     * @description: 实现栈的思路:链表实现
     */
    public class MeOneTypeStack<T> implements Iterable<T> {
    
        /**
         * 首先需要一个头节点
         */
        private Node<T> head;
    
        /**
         * 记录栈中元素的个数
         */
        private int size;
    
        /**
         * 无参构造函数
         */
        public MeOneTypeStack() {
            // 初始化头节点
            this.head = new Node<>(null, null);
            // 初始化元素个数
            this.size = 0;
        }
    
        /**
         * 栈的元素个数获取
         *
         * @return
         */
        public int size() {
            return size;
        }
    
        /**
         * 栈是否为空
         *
         * @return
         */
        public boolean isEmpty() {
            return size == 0;
        }
    
        /**
         * 压栈操作
         * 例子:0->1
         * 0->2->1
         * 0->3->2->1
         *
         * @param t
         */
        public void push(T t) {
            // 如果头节点下一个为空,那么直接加入头节点后面
            if (head.next == null) {
                head.next = new Node<>(t, null);
                // 元素个数自增一
                size = size + 1;
                return;
            }
            // 如果头节下一个节点有值了,那么这时候再加入,需要插入在头节点和下一个节点之间:0->1 再插入一个变成 0->2->1
            Node<T> oldNode;
            Node<T> newNode = new Node<>(t, null);
            // 将头节点后面所有的节点剥离出来
            oldNode = head.next;
            // 将新节点接入到头结点后面
            head.next = newNode;
            // 将老的节点再接到新的节点后面
            newNode.next = oldNode;
            // 元素个数自增一
            size = size + 1;
        }
    
        /**
         * 弹栈操作
         */
        public T pop() {
            // 检查头节点后面有没有数据
            if (head.next == null) {
                return null;
            }
            Node<T> currentNode = head.next;
            // 头节点后面有节点,0->2->1 变成 0->1
            Node<T> oldNode = head.next.next;
            // 将头节点接回到去掉第一个元素后的链表上
            head.next = oldNode;
            // 元素个数减一
            size = size - 1;
            return currentNode.item;
        }
    
    
        private class Node<T> {
    
            /**
             * 当前节点里面的对象
             */
            private T item;
    
            /**
             * 下一个元素
             */
            private Node<T> next;
    
            public Node(T item, Node<T> next) {
                this.item = item;
                this.next = next;
            }
        }
    
        @Override
        public Iterator<T> iterator() {
            return new SIterator();
        }
    
        /**
         * 实现Iterator接口,从而实现栈的增强for循环
         */
        class SIterator implements Iterator {
            // 指针节点
            private Node cursor;
    
            // 初始化指针节点为头节点
            public SIterator() {
                cursor = head;
            }
    
            @Override
            public boolean hasNext() {
                // 只要指针节点的next不等于null就说明还有元素
                return cursor.next != null;
            }
    
            @Override
            public T next() {
                // 从当前指针拿到下一个节点
                Node<T> currNode = cursor.next;
                // 将指针变为下一节点
                cursor = cursor.next;
                // 从当前节点中拿到元素
                return currNode.item;
            }
        }
    }
    

    以上我们就用链表这种数据结构实现了一个MeOneTypeStack栈集合,接下来我们可以开测试一下。

    b.测试代码
    /**
     * @author: 
     * @date: 2022/3/8 10:35
     * @description:
     */
    public class MeOneTypeStackTest {
        public static void main(String[] args) {
            MeOneTypeStack<String> stack = new MeOneTypeStack<>();
            stack.push("A");
            stack.push("B");
            stack.push("C");
            stack.push("D");
            // ABCD顺序压栈后,数据在栈中的顺序应该是DCBA
            for(String s:stack){
                System.out.println(s);
            }
            System.out.println("=======================");
            //弹栈测试
            System.out.println("第一次弹出元素:"+stack.pop());
            for(String s:stack){
                System.out.println(s);
            }
        }
    }
    
    c.测试结果
    D
    C
    B
    A
    =======================
    第一次弹出元素:D
    C
    B
    A
    

    2.数组实现思路

    其实用数组实现会相对链表实现会复杂一下,因为数组你需要初始化容量,如果容量不够了需要进行扩容操作,如果容器中元素过少了,为了节约内存空间,需要进行缩容操作,有点类似于ArrayList集合里面的动态扩容,缩容。现在有一个数组[1,2],我们按照栈的要求,放进数组,我们可以直接顺序放入,比如这个数组再放入3,变成[1,2,3],但是弹栈怎么让3,这个最后进去的先出来呢?这里我们可以利用数组的特性,从后往前遍历,这样我们就能让最后放进数组的元素先拿出来,同时再用一个变量去记录元素的个数即可。

    a.数组实现代码
    /**
     * @author: 
     * @date: 2022/3/8 10:47
     * @description: 数组实现栈
     */
    public class MeTwoTypeStack<T> implements Iterable<T> {
    
        /**
         * 数组
         */
        private T[] elementData;
        /**
         * 栈中的元素个数
         */
        private int elementCount;
        /**
         * 默认数组长度
         */
        private int default_capacity = 10;
    
        /**
         * 无参构造函数
         */
        public MeTwoTypeStack() {
            this.elementData = (T[]) new Object[default_capacity];
            this.elementCount = 0;
        }
    
        /**
         * 获取当前栈的元素个数
         *
         * @return
         */
        public int size() {
            return elementCount;
        }
    
        /**
         * 判断当前栈是否为空
         *
         * @return
         */
        public boolean isEmpty() {
            return elementCount == 0;
        }
    
        /**
         * 压栈 我们就正常往数组中放入元素即可
         * 1,2,3,4
         * 这样放入数组
         *
         * @param t
         */
        public void push(T t) {
            // 如果当前元素个数和数组容器大小一样时,需要对容器进行两倍的扩容
            if (elementCount == elementData.length) {
                reSize(2 * elementData.length);
            }
            elementData[elementCount++] = t;
        }
    
        /**
         * 弹栈,由于弹栈需要先进后出,所以我们从数组的后面开始遍历弹出
         * 这里我们弹栈操作后,数组里面的数据并没有减少,我们仅仅是将最后一个元素的值返回,
         * 我们要用数组元素个数这个变量来控制,下次返回的元素的下标应该是elementCount-1
         *
         * @return
         */
        public T pop() {
            // 如果当前栈中元素为空,则直接返回null
            if (elementCount == 0) {
                return null;
            }
            // 拿到数组最后一位的元素
            T current = elementData[elementCount - 1];
            // 数组的长度减一
            elementCount = elementCount - 1;
            // 元素个数小于等于数组容器长度的四分之一时,需要缩容两倍
            if (elementCount <= elementData.length / 4) {
                reSize(elementData.length / 2);
            }
            return current;
        }
    
        /**
         * 数组的扩容缩容
         *
         * @param capacity
         */
        private void reSize(int capacity) {
            // 创建一个临时数组
            T[] temp = elementData;
            // 原来的数组指向新的扩容或者缩容后的数组
            elementData = (T[]) new Object[capacity];
            // 将原来的数组中的元素赋值到新的数组中
            for (int i = 0; i < elementCount; i++) {
                elementData[i] = temp[i];
            }
        }
    
        @Override
        public Iterator<T> iterator() {
            return new SIterator();
        }
    
        class SIterator implements Iterator {
            // 设置指针
            private int n;
    
            public SIterator() {
                // 初始化指针的位置,从数组末尾开始遍历,所以是数组的最后以为开始
                this.n = elementCount - 1;
            }
    
            @Override
            public boolean hasNext() {
                return n >= 0;
            }
    
            @Override
            public T next() {
                T current = elementData[n];
                n = n - 1;
                return current;
            }
        }
    }
    
    

    以上就是我们数组的实现栈的思路了,可能有人会疑惑,我出栈的操作,并没有让数组中的元素减少,只是让最后一个元素的值返回了,这样不等于没有删除吗,这里我用了元素个数elementCount来标识数组中出栈后应该还剩的元素个数,所以每次出栈操作我都会让元素个数减一,因为元素是按照顺序排列的,下标识固定的。而且在缩容的时候,会重新创建一个新的数组去取代原来的数组,此时就会把里面实际应该清除,但是没有清除的数据让JVM在垃圾回收时将这个没有被引用的对象给清除掉。

    b.测试代码

    加入十一个元素,目的是检验扩容功能有没有问题,我们初始化数组长度是10,如果有问题,在加入第十一个元素的时候就会出现数组越界。

    /**
     * @author: 
     * @date: 2022/3/8 10:35
     * @description:
     */
    public class MeTwoTypeStackTest {
        public static void main(String[] args) {
            MeTwoTypeStack<String> stack = new MeTwoTypeStack<>();
            stack.push("A");
            stack.push("B");
            stack.push("C");
            stack.push("D");
            stack.push("E");
    
            stack.push("F");
            stack.push("G");
            stack.push("H");
            stack.push("I");
            stack.push("J");
    
            stack.push("K");
    
    
            // ABCD顺序压栈后,数据在栈中的顺序应该是DCBA
            for(String s:stack){
                System.out.println(s);
            }
            System.out.println("=======================");
            //弹栈测试
            System.out.println("第一次弹出元素:"+stack.pop());
            for(String s:stack){
                System.out.println(s);
            }
        }
    }
    
    c.测试结果
    K
    J
    I
    H
    G
    F
    E
    D
    C
    B
    A
    =======================
    第一次弹出元素:K
    J
    I
    H
    G
    F
    E
    D
    C
    B
    A
    

    注意:jdk里面的Stack类其实就是用了数组的这种形式实现了栈,它是通过继承Vector集合类,然后使用Vector类的api组合从而实现的栈的这种先进后出的特性。

    总结

    或许你会再次疑惑我们为什么要实现已经有了的数据结构,主要是我们通过自己实现更好的理解设计者的思路,并且这种思路往往可以解决现实中的很多业务场景问题。

    相关文章

      网友评论

          本文标题:手写一个Stack(栈)的集合类(链表实现+数组实现)

          本文链接:https://www.haomeiwen.com/subject/qularrtx.html