上篇文章我们介绍了线性表的相关特性、存储结构以及给出了相应的Java实现,今天我们介绍另一种数据结构栈,其实栈本质是线性表,只不过是运算受限的线性表,接下来我们介绍一下栈,come on


栈的定义
1、栈(stack)又称堆栈,它是运算受限的线性表。
2、栈限制是仅允许在表的一端进行插入和删除操作,不允许在其他任何位置进行插入、查找、删除等操作。
3、表中进行插入、删除操作的一端称为栈顶(top),栈顶保存的元素称为栈顶元素,相对的,表的另一端称为栈底(bottom)。
4、空栈:当栈中没有数据元素时称为空栈;
5、向一个栈插入元素又称为进栈或入栈;
从一个栈中删除元素又称为出栈或退栈。
6、由于栈的插入和删除操作仅在栈顶进行,后进栈的元素必定先出栈,所以又把堆栈称为后进先出表(Last In First Out,简称 LIFO)
栈的存储结构
和线性表类似,堆栈也有两种基本的存储结构:顺序存储结构和链式存储结构。
1、顺序栈
① 顺序栈是使用顺序存储结构实现的堆栈,即利用一组地址连续的存储单元依次存放堆栈中的数据元素。
② 由于堆栈是一种特殊的线性表,因此在线性表的顺序存储结构的基础上,选择线性表的一端作为栈顶即可。
根据数组操作的特性,选择数组下标大的一端,即线性表顺序存储的表尾来作为栈顶,此时入栈、出栈等操作可以在Ο(1)时间完成。
③ 由于堆栈的操作都在栈顶完成,因此在顺序栈的实现中需要附设一个指针 top来动态的指示栈顶元素在数组中的位置。通常top可以用栈顶元素所在数组下标来表示,top= -1 时表示空栈。
MyStack.java:public class MyStack<E> extends MyVector<E> {/*** 压栈:在数组末尾添加元素* @param item* @return*/public E push(E item) {addElement(item);return item;}/*** 出栈:在数组末尾删除元素* @return*/public synchronized E pop() {E obj;int len = size();obj = peek();removeElementAt(len - 1);return obj;}/*** 查看栈顶元素,不删除* @return*/public synchronized E peek() {int len = size();//如果数组为空,则抛出异常if (len == 0)throw new RuntimeException("空栈");return elementAt(len - 1);}}MyVector.java:public class MyVector<E> implements List<E> {//扩容系数protected int capacityIncrement;//元素数组protected Object[] elementData;//数组中元素个数protected int elementCount;//修改次数protected transient int modCount = 0;//最大数组大小private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** 指定初始容量和扩容系数* @param initialCapacity* @param capacityIncrement*/public MyVector(int initialCapacity, int capacityIncrement) {if (initialCapacity < 0)throw new IllegalArgumentException("非法容量大小参数");this.elementData = new Object[initialCapacity];this.capacityIncrement = capacityIncrement;}public MyVector(int initialCapacity) {this(initialCapacity, 0);}/*** ① 默认容量大小是10* ② 这里不同于ArrayList,它是在创建时就分配了容量为10的数组,但是ArrayList在第一次调用add时才生成一个容量为10数组。*/public MyVector() {this(10);}/*** 添加元素:线程安全* @param obj*/public synchronized void addElement(E obj) {modCount++;//扩容ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = obj;}/*** 判断是否需要扩容* @param minCapacity*/private void ensureCapacityHelper(int minCapacity) {// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}/*** 动态扩容:* ① 扩容机制:如果指定了扩容系数capacityIncrement,就按照这个指定的系数扩大容量,扩大后容量为oldCapacity+capacityIncrement,* 如果没有指定就默认扩大到原来一倍。* @param minCapacity*/private void grow(int minCapacity) {int oldCapacity = elementData.length;//如果指定了capacityIncrement,则容量大小为:oldCapacity + capacityIncrement//若未指定,则容量大小为:oldCapacity + oldCapacityint newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0)throw new OutOfMemoryError("数组越界");return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}/*** 返回数组大小* @return*/public synchronized int size() {return elementCount;}/*** 返回指定索引位置元素* @param index* @return*/public synchronized E elementAt(int index) {if (index >= elementCount) {throw new ArrayIndexOutOfBoundsException("下标越界异常");}return elementData(index);}E elementData(int index) {return (E) elementData[index];}/*** 删除指定索引位置元素* @param index*/public synchronized void removeElementAt(int index) {modCount++;//先检查下标是否合法if (index >= elementCount) {throw new ArrayIndexOutOfBoundsException("下标越界异常");}else if (index < 0) {throw new ArrayIndexOutOfBoundsException("下标越界异常");}//获取删除某所索引位置元素后续需要移动的元素个数int j = elementCount - index - 1;if (j > 0) {System.arraycopy(elementData, index + 1, elementData, index, j);}elementCount--;//便于GC回收elementData[elementCount] = null;}}
Stack继承了Vector,Vector和ArrayList的区别是:Vector中的操作是synchronized修饰的,即线程安全的,所以相对的效率会低;我们上篇文章也给出了ArrayList的源码,可以看到两者底层都是维护一个动态数组,但是两者采用的扩容机制不同,①Vector的构造函数可以指定内部数组的初始容量和扩容系数,如果不指定初始容量默认初始容量为10,不同于ArrayList的是它在创建时就分配了容量为10的数组,但是ArrayList在第一次调用add时才生成一个容量为10数组;②在进行扩容的时候,ArrayList是按照原数组的1.5倍扩容,而Vector扩容会先判断是否指定了扩容系数,如果未指定则变为原数组的2倍,否则为原数组长度+扩容系数。
2、链栈
① 可以采用链表的存储结构来实现栈;
② 当采用单链表存储线性表后,根据单链表的操作特性选择单链表的头部作为栈顶,此时,入栈、出栈等操作可以在Ο(1)内完成。
③ 由于堆栈的操作只在线性表的一端进行,在这里使用带头结点的单链表或不带头结点的单链表都可以。
使用带头结点的单链表时,结点的插入和删除都在头结点之后进行;
④在Java中Deque双端队列接口中定义了栈的操作,其中有一个实现类LinkedList给出了栈相关的操作实现,在上篇文章介绍了LinkedList作为链表的一些操作,以下给出实现栈的操作:
public class MyLinkedList<E> implements Deque<E> {transient int size = 0;//头结点transient Node<E> first;//尾节点transient Node<E> last;protected transient int modCount = 0;private static class Node<E>{private Node<E> prev;private E item;private Node<E> next;private Node(Node<E> prev,E item,Node<E> next){this.prev = prev;this.item = item;this.next = next;}}/*** 压栈:在链表头插入元素(表头相当于栈顶)* @param e*/public void push(E e) {addFirst(e);}public void addFirst(E e) {linkFirst(e);}//将插入元素作为首节点private void linkFirst(E e) {final Node<E> f = first;//创建新加入节点为首节点final Node<E> newNode = new Node<E>(null, e, f);first = newNode;//如果是空链表,则尾节点也指向该创建的节点if (f == null)last = newNode;elsef.prev = newNode;size++;modCount++;}/*** 出栈:删除链表首节点* @return*/public E pop() {return removeFirst();}/*** 删除链表首节点,如果链表为空,会抛出异常* @return*/public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);}private E unlinkFirst(Node<E> f) {final E element = f.item;final Node<E> next = f.next;f.item = null;f.next = null;first = next;if (next == null)last = null;elsenext.prev = null;size--;modCount++;return element;}}
双端队列Deque接口继承了队列接口Queue,Queue继承了Collection接口,所以可以看出Deque即定义了栈操作,也定义了队列操作,而LinkedList是其一个实现类,所以既可以作为栈使用,也可以作为队列使用,队列我们在下篇文章会详细介绍。
插个小话题,在看源码的时候很好奇,在子接口中为什么要把父接口的方法又重新定义了一遍呢?eg:Deque继承了Queue,但在Deque中又定义了Queue中已存在的方法,这样似乎违背了面向对象里继承的原则,查了一下说主要原因是一些JDK版本的更新,有的新类是后面新增加的,这些新的接口有的是为了保持兼容,另一个说法是为了增强代码阅读性,让我联想到在工作中有一次遇到一个疑惑点:基于产品进行项目定制开发时,如果产品中某一个后端接口想要修改,为了向下兼容,不能在产品中直接修改此接口,需要将该接口重新复制一份再修改,即新增接口,遵循Java面向对象的开闭原则,刚开始不是很理解为什么要复制一份不能在原先基础上修改,那如果要是遇到很多接口都是这种情况,那都需要复制一份,那代码不会冗余吗,其实等到代码冗余的时候就会进行代码的重构,进行产品大版本的升级,现在稍微明白一些了。
栈就总结到这里,如果大家发现有什么不妥之处或分析不到位的地方,非常欢迎留言讨论交流哦










