数据结构--栈

  栈,和数组或链表一样,也是一种用来存储数据的线性结构,但不同的是,栈对数据的存取有着限制,它遵循着先进后出或后进先出的原则。怎么理解呢?想一想收件箱中的邮件。打开收件箱,邮件是按时间顺序从晚到早时进行排列的,第一封邮件时间最晚,最后一封邮件时间最早,邮件来的越早,它越在收件箱的底部,邮件来的越晚,它越在收件箱的顶部。看邮件的时候,先看第一封邮件,看的是来的最晚的邮件。如果不重要就删除了,这就是后进先出,最晚的邮件最先删除。看了好久,终于快看完了,先进后出。此时,又收到一封邮件,它又放到收件箱的顶端,你发现,无论是添加还是删除,都是在收件箱的顶部操作。如果把收件箱看作栈,你会出现栈的一些特征,栈中元素的排列顺序,正好和它们的添加时间相反,添加的越早,越在栈的下面,添加的越晚,越在栈的上面。添加元素相当于把原有元素向下压,删除元素相当于下面的元素向上顶,把元素弹出去。添加或删除,都在栈的一端操作,栈的顶部操作,而不是随意的操作栈中的元素,如果想获取栈底的元素,只能把它上面的元素都弹出来。元素称为入栈或压栈,删除元素称为出栈或弹栈。

  栈是一种抽象的概念,要怎么实现它的呢?实际上,只要保证先进后出或后进先出的原则,用什么实现都可以,数组,链表,甚至队列,都没有问题。

  数组实现  

  使用数组,怎么保证先进后出或后进先出呢?在数组的尾部进行添加或删除元素就可以了。添加第一个元素放到数组的0位置,添加第二个元素放到数组的1位置 ,删除的时候,先删除第1个位置的元素,再删第0个位置上的元素。  数组的尾部进行操作,保证了后进先出。数组尾部操作也简单,它不会影响数组内的其它元素。根据描述,也可以知道实现栈的类的属性,一个是数组的引用,一个是追踪元素的索引。

构造函数就是要创建数组,进行初始化栈。创建数组要指明数组的长度,长度具体是多少,可以让使用者指定,也可以设一个默认的长度。

public class ArrayStack<T> {
   private T[] stack; // 实现栈的底层数组
   private int topIndex; // 记录栈中或数组中最后一个元素的位置
   private static final int DEFAULT_CAPACITY = 50; // 默认数组的大小

   public ArrayStack() {
      this(DEFAULT_CAPACITY);
   }

   public ArrayStack(int initialCapacity) {
      stack = (T[])new Object[initialCapacity];
      topIndex = 0;
   }
}

  添加元素(入栈),称为push,就是向数组的尾部添加元素,如下图所示

数据结构--栈

   就是数组topIndex 的位置处设为添加的元素,同时topIndex加1。

public void push(String elem) {
    stack[topIndex] = elem;
    topIndex++;
}

  删除元素或出栈,称为pop。就是topInex--,获取到topIndex处的元素,然后把数组这个位置设为null。

数据结构--栈

public T pop(){
   topIndex--;
   String item = stack[topIndex];
   stack[topIndex] = null;

   return item;
}

  当然,如果栈为空的话,是不能执行删除操作的,如果执行删除操作,就报错。这时还需要isEmpty()的方法,同时在pop()中调用

public boolean isEmpty() {
   return topIndex == 0;
}
public T pop(){
   if (isEmpty()){
      throw new RuntimeErrorException("栈为空");
   }
   topIndex--;
   T item = stack[topIndex];
   stack[topIndex] = null;

   return item;
}

  除了主要的push和pop方法外,栈还有其它几个基本方法。peek() 方法, 看一下栈的顶部元素是什么,它只需要获取元素,而不用移动topIndex。和pop()方法一样,如果栈为空,peek()方法报错

public T peek() {
   if(isEmpty()){
      throw  new RuntimeErrorException("栈为空");
   }
   return stack[topIndex - 1];
}

  size()方法,栈中有多少个元素,直接返回topIndex 就可以了。

public int size() {
   return topIndex;
}

  clear(), 清空栈,简单点,就直接让topIndex = 0; 复杂一点,就是调用pop() 方法。

public void clear() {
   topIndex = 0;
}

   数组实现有一个问题,就是是数组的长度是固定的,如果不停地添加元素,数组会满的。这个时候,就要扩容。扩容的实现方式是重新创建一个大数组,把stack数组中的元素,复制到新的数组中,然后让stack指向这个新数组

public void ensureCapcity(){
   T[] bigArray = (T[])new Object[stack.length * 2];
   for(int i = 0; i < stack.length; i++){
      bigArray[i] = stack[i];
   }

   stack = bigArray;
}

  当不断删除元素的时候,数组就需要缩容。缩容和扩容的原理一样,只不过是生成一个小的数组,这时可以把ensureCapcity()方法接受一个参数,扩容和缩容就可以使用一个方法了,把方法名改成resize()

private void resize(int max) { 
        T[] tempArray = (T[])new Object[max];
        for (int i = 0; i < N; i++)
            tempArray[i] = stack[i];
        stack = temp;
    }

   push() 方法,就要选判断数组是不是还有剩余空间,如果没有就扩容,调用resize()方法,扩多大呢?一般是原数组大小的两倍。

public void push(String elem) {
    if (topIndex == stack.length)
        resize(2 * stack.length);
    stack[topIndex] = item;
    topIndex++;
}

  缩容,就是在断删除元素的时候,

public T pop(){
    if (isEmpty()){
       throw new RuntimeErrorException("栈为空");
    }

    topIndex--;
    T item = stack[topIndex];
    stack[topIndex] = null;

    if (topIndex > 0 && topIndex== stack.length / 4) {
        resize(stack.length / 2);
    }

    return item;
 }

  迭代方法的实现

import java.util.Iterator;

public class ArrayStack<T> implements Iterable<T> {

    @Override
    public Iterator<T> iterator() {
        return new ReverseArrayIterator();
    }

    private class ReverseArrayIterator implements Iterator<T> {
        int i = topIndex;
        @Override
        public boolean hasNext() {
            return i > 0;
        }
        @Override
        public T next() {
            return stack[--i];
        }
    }
}

 

上一篇:经典数据结构系列之:栈的应用


下一篇:程序员的浪漫-桃心表白