【Java基础】Java内置数据结构——栈

来源:互联网 发布:这个世界的真相知乎 编辑:程序博客网 时间:2024/05/16 03:38

简述

以前在学习C语言的时候学习过基本的数据结构,栈就是其中非常基础的数据结构之一。
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。--百度百科。先入后出(FILO)是栈最基本的特征。

栈——百度百科
上图来源:百度百科

Java 中的栈结构

一个最基本的栈结构,需要具备以下以下特征和功能。
1.线性结构:栈是一种特殊的线性表,只能对栈顶元素进行操作
2.内部有序:入栈元素按照顺序存储在栈空间中
3.线程安全:只有在对栈顶元素操作成功的情况下,才可以进行下一步操作
根据以上条件,我们的脑海中浮现了与之十分相似的特殊的List——Vector,Vertor是一个线性表、元素有序存储并且线程安全。也就是说,我们完全可以通过Vector来实现一个线程安全的栈数据结构,对栈顶进行操作就是对Vector的最后一个元素进行操作。但是我们完全不需要这么做,因为Java开发者们为我们完成了这个功能,在1.0版本中就已经实现的 Stack 类,而且 Stack 类的底层实现就是 Vector.

public class Stack<E> extends Vector<E>{    public Stack(){    }    ...}

可能是因为功能太过专一,又或者是与Vector 使用少的原因类似,Stack类在Java程序中出现的频率并不高。正如 Java 工程师通过ArrayList 功能代替 Vector 一样,在1.6版本中,Java 工程师们创建了Deque接口以及其实现以提供 FILO 堆栈操作更完整以及更一致操作的 “set”。在软件开发过程中,优先使用Deque接口的实现类。如

Deque<String> stack = new ArrayDeque<String>();

这个具体功能我们后面进行探讨,下面我们来看一下1.0版本的栈类:Stack。

1.0 版本栈结构 Stack 类

字段

Stack 继承了 Vector 类,从 Vector 中继承了几个字段
* elementData :存储向量组件的数组缓冲区 – 我将其看作对象的引用
* elementCount : 对象中的有效组件数
* capacityIncrement : 向量的大小大于其容量时,容量自动增加的量。(与 Vector 一样,Stack 除了初始大小之外,可以自动进行扩容,扩容方式与 Vector 相同:正常情况下为原来大小的2倍)

//可以看作 int newCapacity = oldCapacity * 2 ;int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

还有一个继承自 AbstractList 的 字段 modCount ,用于迭代器的实现时使用,这里不做叙述。

构造方法

Stack 只有一个构造方法,用于初始化一个栈结构的对象。初始化之后的栈,其中不包含任何项。

public Stack(){}

在对 Stack 进行初始化的时候,我们可以为它设定一个泛型,如

Stack<String> stack = new Stack<>();//Stack<E> stack = new Stack<>();//我们要注意的是,因为Vector类中要求其内容不可以为基本数据类型,所以在 Stack 的构造方法中也有这样的约定。
方法

在1.0版本的 Stack 类中,提供了以下5个方法来对其进行操作:
+ push(E item) 返回值 E

向栈顶压入元素,效果等价于Vector.addElement(E);包括直接使用Stack.addElement(E)方法也都是可以的~甚至在有些时候 .add 方法也可以完成同样的功能

Stackpublic E push(E item) {        addElement(item); //底层实现就是Vector.addElement(E)方法~        return item;}Vectorpublic synchronized void addElement(E obj) {        modCount++; //用于 迭代器        ensureCapacityHelper(elementCount + 1); //扩容        elementData[elementCount++] = obj; //将Vector的最后一个元素设定为obj }
  • pop() 返回值 E
    从栈顶取出栈顶元素,将其从栈中移除并将其作为函数返回值返回
public synchronized E pop() {    E       obj;    int     len = size(); //size() 方法实际上是将栈中的元素数量返回,并赋值给 len.    obj = peek(); //调用 peek()方法,将栈顶元素返回    removeElementAt(len - 1);  //调用Vector.removeElementAt(int index)方法    return obj;}//Vector.removeElementAt(int index) 方法 //其中 index < 0 || index >= size() 不然会抛出异常public synchronized void removeElementAt(int index) {    modCount++;    if (index >= elementCount) {      throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);    }    else if (index < 0) {        throw new ArrayIndexOutOfBoundsException(index);    }     //以上都为异常情况    int j = elementCount - index - 1;//正常情况下 j = elementCount - (len - 1) - 1=len - len + 1 - 1 = 0    if (j > 0) { //如果 j > 0 ,表示传入参数的 len 要小于Vector中的元素数量,那么就将原来index(len - 1)位置下的元素去除。        System.arraycopy(elementData, index + 1, elementData, index, j);     } // Java 工程师采用队列复制的方法,将index位置下的数据去除。    elementCount--; //线性表中元素数量-1    elementData[elementCount] = null; //将线性表的最后一个元素设定为null~,即去除栈顶元素}
  • peek() 返回值 E
    将栈顶元素返回,但是不从栈中移除它
public synchronized E peek() {        int     len = size();        if (len == 0)            throw new EmptyStackException(); //空栈,抛出异常        return elementAt(len - 1); // Vector 的方法,返回相应位置的元素——最后一个元素 的值    }
  • empty() 返回值 boolean
    判断栈是否为空——size() == 0 ? ture : false;
  • search(Object obj); 返回值 int - 基数为1
    返回对象在堆栈中的位置,以 1 为基数。如果对象 obj 是堆栈中的一个项,此方法返回距堆栈顶部最近的出现位置到堆栈顶部的距离;堆栈中最顶部项的距离为 1。
    使用 equals 方法比较 obj 与堆栈中的项。
    Stack.search(Object obj) 是调用的 Vector.lastIndexOf(Object obj) 方法
return size() - Vector.lastIndexOf(obj) ;  //整理后的伪代码
使用样例

在这里,我们来借用lintcode.com中的一个题目来展现Stack类的使用;
【有效的括号序列】
* 给定一个字符串所表示的括号序列,包含以下字符: ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[’ and ‘]’, 判定是否是有效的括号序列。

public class ValidParentheses{     public boolean isValidParentheses(String s) {        // write your code here        if (0 != s.length() % 2) {            return false;        }        if (0 == s.length()) {            return true;        }        char[] ch = s.toCharArray();        //创建一个堆栈        Stack<String> stack = new Stack<>();        //先向堆栈中压入第一个字符        stack.push(ch[0] + "");        //循环        for (int i = 1; i < ch.length; i++) {            if (stack.isEmpty()) {                stack.push(ch[i] + ""); //将 char 转换 为 String                 continue;            }            //使用peek()方法取出栈顶元素,分别于右半边括号进行匹配,如果匹配成功就移除栈顶元素,不成功就将这个元素压入栈顶            if ("(".equals(stack.peek()) && (ch[i] + "").equals(")")) {                  stack.pop();                continue;            }            if ("[".equals(stack.peek()) && (ch[i] + "").equals("]")) {                stack.pop();                continue;            }            if ("{".equals(stack.peek()) && (ch[i] + "").equals("}")) {                stack.pop();                continue;            } else {                stack.push(ch[i] + "");            }        }        //如果最后空栈,那么表示这是个正确的括号序列 true,如果不为空,则表示序列不符合要求,返回false.        return stack.isEmpty();    }}

1.6 版本栈结构 Deque 接口

public interface Deque<E> extends Queue<E> { ... }

Deque 是一个线性的collection,支持在两端插入和移除元素。deque 是 “double ended queue(双端队列)” 的缩写。
大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。 ——JDK 1.6 API

直接或间接父类
  • Collection : Deque 是 Java 集合类系统接口的一部分
  • Iterable : 继承了 Iterable ,表示 Deque 的实现类可以使用迭代器
  • Queue : 这是个线性队列
常用方法
栈顶元素 栈顶元素 栈底元素 栈底元素 抛出异常 特殊值 抛出异常 特殊值 插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e) 移除 removeFirst() pollFirst() removeLast() pollLast() 检查 getFirst() peekFirst() getLast() peekLast()
实现类

Deque 具有双端队列特性,那么他就可以有以下两种实现:
* 作为 FIFO 的队列 —— LinkedList 类
* 作为 FILO 的栈 —— ArrayDeque 类

LinkedList 链表类

链表是 Deque 的 FIFO 的典型实现,在这里我们着重探讨“栈”,这里只对LinkedList 做简单叙述。
在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。
- add(E) == addLast(e) : 从后端添加元素
- remove() == removeFirst() : 从开头移除元素
- poll() == pollFirst() : 获取并移除开头元素
- peek() == peekFirst() : 获取但是不移除开头元素

ArrayDeque 栈类

双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。

堆栈方法 等效 Deque 方法 push(e) addFirst(e) pop() removeFirst() peek() peekFirst()

注意,在将双端队列用作队列或堆栈时,peek方法同样正常工作;无论哪种情况下,都从双端队列的开头抽取元素。
——————————————————————————————————————————————

public ArrayDeque() {        elements = new Object[16];}

首次使用的时候,可以初始化一个大小为16的双端队列
—————————————————————————————————————————————–

使用举例

在下面,我们通过使用 ArrayDeque 类,用一段伪代码来完成“有效括号序列”这个题目。

public boolean isValidParentheses(String s) {    if (s.length() < 2 || 0  != s.length() % 2) {  //s的长度是单数,返回 false        return false;    }    char[] ch = s.toCharArray();    //初始化一个 ArrayDuque ,用来存储数据    ArrayDeque<char> arrayDeque = new ArrayDeque<>();    for (char x : ch) {        if (arrayDeque.isEmpty()) {            arrayDeque.push(x);            continue;        }         //用堆栈中的头元素与括号右半边进行匹配,匹配成功就从栈中取出元素,不然就将此元素放入栈中        else if ('('.equals(arrayDeque.peek()) && ')'.equals(x) {            arrayDeque.pop();            continue;        } else {            arrayDeque.push(x);            continue;        }    }    return arrayDeque.isEmpty();}

具体实现和使用 Stack 相差无几,但是在速度上ArrayDeque 要快很多,这是由他们的底层代码所决定的。

其他应用

以上只是 ArrayDeque 用作栈时应用的一个举例。但是 ArrayDeque 是一个io较快的(它是线程不安全的,所以它本身没有很多锁来限制速度)双端队列,所以我想,我们可以使用 ArrayDeque 来实现一个特殊的,流水线式的数据结构。我们只通过两个单独的锁来控制这个队列的入队数据与出队数据一致以保证数据有序,队列中不添加任何处理。当然,这是我个人想法,或许已经实现或许已经被验证失败,这里只是提供一个思路,拓宽我们的思维而已~

最后

Java 在公共lib中实现了很多数据结构,我会在以后一点一点的对其中的一部分进行解析,拓宽思维充实基础,当然了,最重要的是填坑……欢迎大家的监督
话说我当时为什么挖那么大坑留给自己 Orz
挖好的坑——【我的第一篇简书,也算是我的2017学习计划】

最后的最后

转载请注明出处~
但是我知道是不会有人转载的 :)

原创粉丝点击