暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

数据结构之栈

ITSK 2019-10-27
656

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

栈的定义
1、栈(stack)又称堆栈,它是运算受限的线性表。
2、栈限制是仅允许在表的一端进行插入和删除操作,不允许在其他任何位置进行插入、查找、删除等操作。
3、表中进行插入、删除操作的一端称为栈顶(top),栈顶保存的元素称为栈顶元素,相对的,表的另一端称为栈底(bottom)。
4、空栈:当栈中没有数据元素时称为空栈;
5、向一个栈插入元素又称为进栈或入栈;
     从一个栈中删除元素又称为出栈或退栈。
6、由于栈的插入和删除操作仅在栈顶进行,后进栈的元素必定先出栈,所以又把堆栈称为后进先出表(Last In First Out,简称 LIFO)

栈的存储结构

和线性表类似,堆栈也有两种基本的存储结构:顺序存储结构和链式存储结构。
1、顺序栈
① 顺序栈是使用顺序存储结构实现的堆栈,即利用一组地址连续的存储单元依次存放堆栈中的数据元素。
② 由于堆栈是一种特殊的线性表,因此在线性表的顺序存储结构的基础上,选择线性表的一端作为栈顶即可。
    根据数组操作的特性,选择数组下标大的一端,即线性表顺序存储的表尾来作为栈顶,此时入栈、出栈等操作可以在Ο(1)时间完成。
③ 由于堆栈的操作都在栈顶完成,因此在顺序栈的实现中需要附设一个指针 top来动态的指示栈顶元素在数组中的位置。通常top可以用栈顶元素所在数组下标来表示,top= -1 时表示空栈。

④ Java中的Stack实现了栈操作,底层采用的是数组,手写了核心代码如下:
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 code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}


/**
* 动态扩容:
* ① 扩容机制:如果指定了扩容系数capacityIncrement,就按照这个指定的系数扩大容量,扩大后容量为oldCapacity+capacityIncrement,
* 如果没有指定就默认扩大到原来一倍。
* @param minCapacity
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//如果指定了capacityIncrement,则容量大小为:oldCapacity + capacityIncrement
//若未指定,则容量大小为:oldCapacity + oldCapacity
int 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;
else
f.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;
else
next.prev = null;
size--;
modCount++;
return element;
}
}

双端队列Deque接口继承了队列接口Queue,Queue继承了Collection接口,所以可以看出Deque即定义了栈操作,也定义了队列操作,而LinkedList是其一个实现类,所以既可以作为栈使用,也可以作为队列使用,队列我们在下篇文章会详细介绍。

插个小话题,在看源码的时候很好奇,在子接口中为什么要把父接口的方法又重新定义了一遍呢?eg:Deque继承了Queue,但在Deque中又定义了Queue中已存在的方法,这样似乎违背了面向对象里继承的原则,查了一下说主要原因是一些JDK版本的更新,有的新类是后面新增加的,这些新的接口有的是为了保持兼容,另一个说法是为了增强代码阅读性,让我联想到在工作中有一次遇到一个疑惑点:基于产品进行项目定制开发时,如果产品中某一个后端接口想要修改,为了向下兼容,不能在产品中直接修改此接口,需要将该接口重新复制一份再修改,即新增接口,遵循Java面向对象的开闭原则,刚开始不是很理解为什么要复制一份不能在原先基础上修改,那如果要是遇到很多接口都是这种情况,那都需要复制一份,那代码不会冗余吗,其实等到代码冗余的时候就会进行代码的重构,进行产品大版本的升级,现在稍微明白一些了。


 栈就总结到这里,如果大家发现有什么不妥之处或分析不到位的地方,非常欢迎留言讨论交流哦




欢迎关注ITSK,每天进步一点点,我们追求在交流中收获成长和快乐


文章转载自ITSK,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论