数据结构-线性表-栈

325 阅读8分钟

概念

关于“栈”这种数据结构,它有一个典型的特点:先进后出,后进先出;只要满足这种特点的数据结构我们就可以说这是典型的“栈”数据结构,我们一般将这个特点归纳为一个:后进先出,英文表示为:Last In First Out 即 LIFO。为了更好的理解栈这种数据结构,我们以一幅图的形式来表示,如下:

我们从栈的操作特点上来看,似乎受到了限制,的确,栈就是一种操作受限的线性表,只允许在栈的一端进行数据的插入和删除,这两种操作分别叫做入栈和出栈。当某个数据集合如果只涉及到在其一端进行数据的插入和删除操作,并且满足先进后出,后进先出的特性时,我们应该首选栈这种数据结构来进行数据的存储。

栈的实现

从对栈的定义中我们发现栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个元素和在栈底删除一个元素,那理解了这个定义之后,我们来思考一个问题,如何来手动实现一个栈呢?

实际上,栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈叫顺序栈,用链表实现的叫链式栈。接下来我们就这两种形式分别去实现一下。

基于数组顺序栈的实现

/**
 * 基于数组的顺序栈的实现 不支持扩容
 * @author houxiangle0205@163.com
 */
public class ArrayStack {
    /** 栈大小*/
    private int size;
    /** 默认栈容量*/
    private final int DEFAULT_CAPACITY = 10;
    /** 栈数据*/
    private Object[] elements;
    /** 栈最大容量*/
    private final int MAX_ARRAY_SIZE = Integer.MAX_VALUE-8;

    /**
     * 默认构造创建大小为10的栈
     */
    public ArrayStack(){
        elements = new Object[DEFAULT_CAPACITY];
    }

    /**
     * 创建指定大小的栈
     * @param capacity 栈的大小
     */
    public ArrayStack(int capacity){
        elements = new Object[capacity];
    }

    /**
     * 检查栈容量是否还够
     * @param minCapacity 需要的栈容量
     */
    private void checkCapacity(int minCapacity){
        if(elements.length - minCapacity < 0){
            throw new RuntimeException("栈容量不够!");
        }
    }

    /**
     * 入栈
     * @param element 添加的数据
     * @return 添加结果
     */
    public boolean push(Object element){
        try {
            checkCapacity(size+1);
            elements[size++] = element;
            return true;
        }catch (RuntimeException e){
            return false;
        }
    }

    /**
     * 出栈
     * @return 获取的数据
     */
    public Object pop(){
        if(size<=0){
            //栈为空则直接返回null
            return null;
        }
        Object obj = elements[size-1];
        elements[--size] = null;
        return obj;
    }

    /**
     * 获取栈的大小
     * @return 栈的大小
     */
    public int size(){
        return size;
    }
}

编写测试代码如下:

public class ArrayStackTest {
    public static void main(String[] args) {
        ArrayStack stack = new ArrayStack();
        for (int i = 0; i < 13; i++) {
            boolean push = stack.push(i);
            System.out.println("第"+(i+1)+"次存储数据为:"+i+",存储结果是:"+push);
        }
        for (int i = 0; i < 11; i++) {
            Object pop = stack.pop();
            System.out.println(pop);
        }
    }
}

打印结果如下:

第1次存储数据为:0,存储结果是:true
第2次存储数据为:1,存储结果是:true
第3次存储数据为:2,存储结果是:true
第4次存储数据为:3,存储结果是:true
第5次存储数据为:4,存储结果是:true
第6次存储数据为:5,存储结果是:true
第7次存储数据为:6,存储结果是:true
第8次存储数据为:7,存储结果是:true
第9次存储数据为:8,存储结果是:true
第10次存储数据为:9,存储结果是:true
第11次存储数据为:10,存储结果是:false
第12次存储数据为:11,存储结果是:false
第13次存储数据为:12,存储结果是:false
9
8
7
6
5
4
3
2
1
0
null

根据打印结果和代码可知:因为是基于数组实现的栈,数组长度是固定的,所以后三次数据的添加全部失败了。

支持动态扩容的顺序栈

刚才那个基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。那我们如何基于数组实现一个可以支持动态扩容的栈呢?

让我们来回顾一下之前查看过的ArrayList源码,是如何实现数组的动态扩容的。当数组空间不够时,就重新申请一块更大的内存,将原来的数据统统拷贝过去。这样就实现了一个支持动态扩容的数组。

所以,如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。

下面是在前面顺序栈的基础之上添加了支持动态扩容之后的代码:

/**
 * 基于数组的顺序栈的实现 支持动态扩容
 * @author houxiangle0205@163.com
 */
public class ArrayStackGrow {
    /** 栈大小*/
    private int size;
    /** 默认栈容量*/
    private final int DEFAULT_CAPACITY = 10;
    /** 栈数据*/
    private Object[] elements;
    /** 栈最大容量*/
    private final int MAX_ARRAY_SIZE = Integer.MAX_VALUE-8;

    /**
     * 默认构造创建大小为10的栈
     */
    public ArrayStackGrow(){
        elements = new Object[DEFAULT_CAPACITY];
    }

    /**
     * 创建指定大小的栈
     * @param capacity 栈的大小
     */
    public ArrayStackGrow(int capacity){
        elements = new Object[capacity];
    }

    /**
     * 检查栈容量是否还够 容量不够就扩容
     * @param minCapacity 需要的栈容量
     */
    private void checkCapacity(int minCapacity){
        if(elements.length - minCapacity < 0){
//            throw new RuntimeException("栈容量不够!");
            grow(elements.length);
        }
    }

    /**
     * 扩容
     * @param oldCapacity 原始容量
     */
    private void grow(int oldCapacity){
        int newCapacity = oldCapacity+(oldCapacity>>1);
        if(newCapacity-oldCapacity<0){
            newCapacity = DEFAULT_CAPACITY;
        }
        if(newCapacity-MAX_ARRAY_SIZE>0){
            newCapacity =  hugeCapacity(newCapacity);
        }
        elements = Arrays.copyOf(elements,newCapacity);
    }

    /**
     * 检查扩容后的数组是否超过最大长度
     * @param newCapacity 扩容后的数组长度
     * @return 没有超过就返回原数据 如果超过了就改为最大长度
     */
    private int hugeCapacity(int newCapacity){
        return Math.min(newCapacity, MAX_ARRAY_SIZE);
    }

    /**
     * 入栈
     * @param element 添加的数据
     * @return 添加结果
     */
    public boolean push(Object element){
        try {
            checkCapacity(size+1);
            elements[size++] = element;
            return true;
        }catch (RuntimeException e){
            return false;
        }
    }

    /**
     * 出栈
     * @return 获取的数据
     */
    public Object pop(){
        if(size<=0){
            //栈为空则直接返回null
            return null;
        }
        Object obj = elements[size-1];
        elements[--size] = null;
        return obj;
    }

    /**
     * 获取栈的大小
     * @return 栈的大小
     */
    public int size(){
        return size;
    }
}

修改测试代码如下:

/**
 * @author houxiangle0205@163.com
 */
public class ArrayStackTest {
    public static void main(String[] args) {
        ArrayStackGrow stack = new ArrayStackGrow();
        for (int i = 0; i < 40; i++) {
            boolean push = stack.push(i);
            System.out.println("第"+(i+1)+"次存储数据为:"+i+",存储结果是:"+push);
        }
        for (int i = 0; i < 11; i++) {
            Object pop = stack.pop();
            System.out.println(pop);
        }
    }
}

控制台打印结果较多,就不写出来了,根据测试结果可知,支持动态扩容的栈是可用的。

基于链表的链式栈的实现

基于链表的栈跟基于数组的顺序栈有一个很大的区别就是链式栈天生就具备动态扩容的特点。

我们知道链表中的每一个元素都可以称之为节点,因此我们先创建一个节点对象如下:

/**
 * 节点类
 * @author houxiangle0205@163.com
 */
public class Node {
    /** 前驱节点*/
    public Node prev;
    /** 节点数据*/
    private Object data;
    /** 后继节点*/
    public Node next;

    public Node(Node prev,Object data,Node next){
        this.prev = prev;
        this.data = data;
        this.next = next;
    }

    public Object getData(){
        return data;
    }
}

接下来实现一个链式栈,提供出栈和入栈的操作:

/**
 * 基于双向链表的链式栈实现
 * @author houxiangle0205@163.com
 */
public class LinkedListStack {
    /** 栈大小*/
    private int size;
    /** 存储链表尾节点*/
    private Node tail;

    public LinkedListStack(){
        this.tail = null;
    }

    /**
     * 入栈
     * @param data 需要添加的数据
     * @return 添加结果
     */
    public boolean push(Object data){
        Node newNode = new Node(tail,data,null);
        if(size>0){
            tail.next = newNode;
        }
        tail = newNode;
        size++;
        return true;
    }

    /**
     * 出栈
     * @return 获取的数据
     */
    public Object pop(){
        if((size-1)<0){
            //栈为空 返回null
            return null;
        }
        Object data = tail.getData();
        tail = tail.prev;
        if(tail!=null){
            tail.next = null;
        }
        size--;
        return data;
    }
}

从以上的实现我们可以看出,我们采用的是双向链表来实现的链式栈,入栈和出栈操作虽然都很快速,但是由于双向链表需要额外的存储空间来存储前驱指针,因此我们这段程序在空间资源的节省上显得力度不够,所以我们想能不能不用双向链表只用单向链表来解决这个问题呢?答案是肯定的,接下来我们就针对刚刚的代码做出改造

首先改造我们的节点对象,每个节点对象中只存储数据和next 指针,并且上面的实现我们是单独创建的节点对象,那现在我们将节点对象声明为栈的内部类

/**
 * 基于单链表实现的栈
 * @author houxiangle0205@163.com
 */
public class StackBasedOnLinkedList {
    /** 存储链表头节点*/
    private NodeOneWay head;

    public StackBasedOnLinkedList(){
        this.head = null;
    }

    /**
     * 入栈
     * @param data 需要往栈中添加的数据
     * @return 添加结果
     */
    public boolean push(Object data){
        NodeOneWay newNode = new NodeOneWay(data,head);
        head = newNode;
        return true;
    }

    /**
     * 出栈
     * @return 获取的数据
     */
    public Object pop(){
        if(head == null){
            return null;
        }
        NodeOneWay topNode = head;
        head = topNode.next;
        topNode.next = null;
        return topNode.data;
    }

    /**
     * 单向节点
     */
    private static class NodeOneWay{
        /** 节点数据*/
        private Object data;
        /** next 指针*/
        private NodeOneWay next;

        public NodeOneWay(Object data, NodeOneWay next){
            this.data = data;
            this.next = next;
        }

        public Object getData(){
            return data;
        }
    }
}

然后我们编写测试类如下:

/**
 * @author houxiangle0205@163.com
 */
public class StackBasedOnLinkedListTest {
    public static void main(String[] args) {
        StackBasedOnLinkedList stack = new StackBasedOnLinkedList();
        for (int i = 0; i < 6; i++) {
            stack.push(i+"");
            System.out.println("第"+(i+1)+"次入栈,入栈的值为:"+i);
        }
        for (int i = 0; i < 8; i++) {
            Object pop = stack.pop();
            System.out.println("取出的结果:"+pop);
        }
    }
}

通过测试我们发现我们基于单链表实现的链式栈跟我们用双向链表实现的链式栈在功能上都是一样的,但是基于单链表实现的链式栈很明显更节约内存空间。

总结

栈就是一种操作受限的线性表,只允许在栈的一端进行数据的插入和删除,这两种操作分别叫做入栈和出栈。当某个数据集合如果只涉及到在其一端进行数据的插入和删除操作,并且满足先进后出,后进先出的特性时,我们应该首选栈这种数据结构来进行数据的存储。

本文已在CSDN同步上传:数据结构-线性表-栈