1.栈的介绍

  在许多算法设计中都需要一种"先进后出(First Input Last Output)"的数据结构,因而一种被称为"栈"的数据结构被抽象了出来。

  栈的结构类似一个罐头:只有一个开口;先被放进去的东西沉在底下,后放进去的东西被放在顶部;想拿东西必须按照从上到下的顺序进行操作。

       示意图来自《大话数据结构》

  对于一个类似罐头的栈,用户能对其进行的操作很少:仅仅可以对栈顶开口处元素进行操作,因而栈的使用方式非常简单。

2.栈的ADT接口

/**
* 栈ADT 接口定义
* */
public interface Stack<E>{ /**
* 将一个元素 加入栈顶
* @param e 需要插入的元素
* @return 是否插入成功
* */
boolean push(E e); /**
* 返回栈顶元素,并且将其从栈中移除(弹出)
* @return 当前栈顶元素
* */
E pop(); /**
* 返回栈顶元素,不将其从栈中移除(窥视)
* @return 当前栈顶元素
* */
E peek(); /**
* @return 返回当前栈中元素的个数
*/
int size(); /**
* 判断当前栈是否为空
* @return 如果当前栈中元素个数为0,返回true;否则,返回false
*/
boolean isEmpty(); /**
* 清除栈中所有元素
* */
void clear(); /**
* 获得迭代器
* */
Iterator<E> iterator();
}

3.栈的实现

  如果我们将开口朝上的栈旋转90度,会发现栈和先前我们介绍过的线性表非常相似。栈可以被视为一个只能在某一端进行操作的,被施加了特别限制的线性表。

3.1 栈的向量实现

  栈作为一种特殊的线性表,使用向量作为栈的底层实现是很自然的(向量栈)。

  jdk的栈结构(Stack)是通过继承向量类(Vector)来实现的,这一栈的实现方式被java集合框架(Collection Framework)的作者Josh Bloch在其所著书籍《Effective Java》中所批评,Josh Bloch认为这是一种糟糕的实现方式,因为继承自向量的栈对使用者暴露了过多的细节

原文部分摘录:

复合优先于继承

  继承打破了封装性。  

  java对象中违反这条规则的:stack不是vector,所以stack不应该扩展vector。如果在合适用复合的地方用了继承,会暴露实现细节。

  继承机制会把超类中所有缺陷传递到子类中,而复合则允许设计新的API来隐藏这些缺陷。

  考虑到这一点,我们的向量栈采用复合的方式实现。通过使用之前我们已经实现的向量数据结构作为基础,实现一个栈容器。

向量栈基本属性和接口:

/**
* 向量为基础实现的 栈结构
* */
public class VectorStack <E> implements Stack<E>{ /**
* 内部向量
* */
private ArrayList<E> innerArrayList; /**
* 默认构造方法
* */
public VectorStack() {
this.innerArrayList = new ArrayList<>();
} /**
* 构造方法,确定初始化时的内部向量大小
* */
public VectorStack(int initSize) {
this.innerArrayList = new ArrayList<>(initSize);
} @Override
public int size() {
return innerArrayList.size();
} @Override
public boolean isEmpty() {
return innerArrayList.isEmpty();
} @Override
public void clear() {
innerArrayList.clear();
} @Override
public Iterator<E> iterator() {
return innerArrayList.iterator();
} @Override
public String toString() {
return innerArrayList.toString();
}
}

  由于我们的向量容器已经具备了诸如自动扩容等特性,因而向量栈的许多接口都可以通过简单的调用内部向量的接口来实现,不需要额外的操作。

栈的特有接口实现:

    @Override
public boolean push(E e) {
//:::将新元素插入内部向量末尾(入栈)
innerArrayList.add(e); return true;
} @Override
public E pop() {
if(this.isEmpty()){
throw new CollectionEmptyException("Stack already empty");
} //:::内部向量末尾下标
int lastIndex = innerArrayList.size() - 1; //:::将向量末尾处元素删除并返回(出栈)
return innerArrayList.remove(lastIndex);
} @Override
public E peek() {
if(this.isEmpty()){
throw new CollectionEmptyException("Stack already empty");
} //:::内部向量末尾下标
int lastIndex = innerArrayList.size() - 1; //:::返回向量末尾处元素(窥视)
return innerArrayList.get(lastIndex);
}

  栈的FIFO的特性,使得我们必须选择内部线性表的一端作为栈顶。

  由于向量在头部的插入/删除需要批量移动内部元素,时间复杂度为O(n);而向量尾部的插入/删除由于避免了内部元素的移动,时间复杂度为O(1)

  而栈顶的元素是需要频繁插入(push)和删除(pop)的。出于效率的考虑,我们将向量的尾部作为栈顶,使得向量栈的出栈、入栈操作都达到了优秀的常数时间复杂度O(1)

3.2 栈的链表实现

  链表和向量同为线性表,因此栈的链表实现和向量实现几乎完全雷同。

  由于链表在头尾出的增加/删除操作时间复杂度都是O(1),理论上链表栈的栈顶放在链表的头部或者尾部都可以。为了和向量栈实现保持一致,我们的链表栈也将尾部作为栈顶。

/**
* 链表为基础实现的 栈结构
* */
public class LinkedListStack<E> implements Stack<E>{ /**
* 内部链表
* */
private LinkedList<E> innerLinkedList; /**
* 默认构造方法
* */
public LinkedListStack() {
this.innerLinkedList = new LinkedList<>();
} @Override
public boolean push(E e) {
//:::将新元素插入内部链表末尾(入栈)
innerLinkedList.add(e); return true;
} @Override
public E pop() {
if(this.isEmpty()){
throw new CollectionEmptyException("Stack already empty");
} //:::内部链表末尾下标
int lastIndex = innerLinkedList.size() - 1; //:::将链表末尾处元素删除并返回(出栈)
return innerLinkedList.remove(lastIndex);
} @Override
public E peek() {
if(this.isEmpty()){
throw new CollectionEmptyException("Stack already empty");
} //:::内部链表末尾下标
int lastIndex = innerLinkedList.size() - 1; //:::返回链表末尾处元素(窥视)
return innerLinkedList.get(lastIndex);
} @Override
public int size() {
return innerLinkedList.size();
} @Override
public boolean isEmpty() {
return innerLinkedList.isEmpty();
} @Override
public void clear() {
innerLinkedList.clear();
} @Override
public Iterator<E> iterator() {
return innerLinkedList.iterator();
} @Override
public String toString() {
return innerLinkedList.toString();
}
}

4.栈的性能

  栈作为线性表的限制性封装,其性能和其内部作为基础的线性表相同。

  空间效率:

    向量栈的空间效率和内部向量相似,效率很高。

    链表栈的空间效率和内部链表相似,效率略低于向量栈。

  时间效率:

    栈的常用操作,poppushpeek都是在线性表的尾部进行操作。因此无论是向量栈还是链表栈,栈的常用操作时间复杂度都为O(1),效率很高。

5.栈的总结

  虽然从理论上来说,栈作为一个功能上被限制了的线性表,完全可以被线性表所替代。但相比线性表,栈结构屏蔽了线性表的下标等细节,只对外暴露出必要的接口。栈的引入简化了许多程序设计的复杂度,让使用者的思维能够聚焦于算法逻辑本身而不是其所依赖数据结构的细节。

  通常,暴露出不必要的内部细节对于使用者是一种沉重的负担。简单为美,从栈的设计思想中可见一斑。

  这篇博客的代码在我的 github上:https://github.com/1399852153/DataStructures,文章还存在许多不足之处,请多指教。

自己动手实现java数据结构(三) 栈的更多相关文章

  1. 自己动手实现java数据结构(一) 向量

    1.向量介绍 计算机程序主要运行在内存中,而内存在逻辑上可以被看做是连续的地址.为了充分利用这一特性,在主流的编程语言中都存在一种底层的被称为数组(Array)的数据结构与之对应.在使用数组时需要事先 ...

  2. JAVA数据结构系列 栈

    java数据结构系列之栈 手写栈 1.利用链表做出栈,因为栈的特殊,插入删除操作都是在栈顶进行,链表不用担心栈的长度,所以链表再合适不过了,非常好用,不过它在插入和删除元素的时候,速度比数组栈慢,因为 ...

  3. 自己动手实现java数据结构(四)双端队列

    1.双端队列介绍 在介绍双端队列之前,我们需要先介绍队列的概念.和栈相对应,在许多算法设计中,需要一种"先进先出(First Input First Output)"的数据结构,因 ...

  4. 自己动手实现java数据结构(二) 链表

    1.链表介绍 前面我们已经介绍了向量,向量是基于数组进行数据存储的线性表.今天,要介绍的是线性表的另一种实现方式---链表. 链表和向量都是线性表,从使用者的角度上依然被视为一个线性的列表结构.但是, ...

  5. 自己动手实现java数据结构(六)二叉搜索树

    1.二叉搜索树介绍 前面我们已经介绍过了向量和链表.有序向量可以以二分查找的方式高效的查找特定元素,而缺点是插入删除的效率较低(需要整体移动内部元素):链表的优点在于插入,删除元素时效率较高,但由于不 ...

  6. 自己动手实现java数据结构(五)哈希表

    1.哈希表介绍 前面我们已经介绍了许多类型的数据结构.在想要查询容器内特定元素时,有序向量使得我们能使用二分查找法进行精确的查询((O(logN)对数复杂度,很高效). 可人类总是不知满足,依然在寻求 ...

  7. Java数据结构之栈(Stack)

    1.栈(Stack)的介绍 栈是一个先入后出(FILO:First In Last Out)的有序列表. 栈(Stack)是限制线性表中元素的插入和删除只能在同一端进行的一种特殊线性表. 允许插入和删 ...

  8. java数据结构-07栈

    一.什么是栈 栈是一种线性结构,栈的特点就是先进后出(FILO):就像弹夹装子弹一样,最先压进去的在最底下,最后才被射出.  二.相关接口设计  三.栈的实现 栈可以用之前的数组.链表等设计,这里我使 ...

  9. 自己动手实现java数据结构(九) 跳表

    1. 跳表介绍 在之前关于数据结构的博客中已经介绍过两种最基础的数据结构:基于连续内存空间的向量(线性表)和基于链式节点结构的链表. 有序的向量可以通过二分查找以logn对数复杂度完成随机查找,但由于 ...

随机推荐

  1. Node.js web发布到AWS ubuntu 之后,关闭Putty,Node 项目也随之关闭的解决办法

    最近公司把BlockChain和对应的Node Web都发布到了AWS 的ubuntu 系统上. 但是遇到了一个问题,每次启动 Node Web之后,关闭Putty,Node Web也随之关闭. 由于 ...

  2. sql server导出大批量数据

    使用sqlserver导出数据的时候,如果数据量大于65536那么就要使用xlsx,最大行数为104万 如果导出的时候报错,则需要在本机安装以下程序: https://www.cnblogs.com/ ...

  3. java30

    1.类的组合关系 当一个类中的字段是一个类时,就称类依赖于字段这个类,也称这两个类为组合关系 2.快捷键:ctrl+shift+c,多行的// ctrl+shift+/,多行的/-----/ 3.类的 ...

  4. python基本数据类型之列表和元组

    python基本数据类型之列表与元组 python中list与tuple都是可以遍历类型.不同的是,list是可以修改的,而元组属于不可变类型,不能修改. 列表和元组中的元素可以是任意类型,并且同一个 ...

  5. python之os库

    python之os库 os.name 判断现在正在实用的平台,Windows 返回 'nt'; Linux 返回'posix' >>> os.name 'nt' os.getcwd( ...

  6. SSM_CRUD新手练习(9)显示分页数据

    我们已经做好了用来显示数据的分页模板,现在只需要将我们从后台取出的数据填充好,显示出来. 我们使用<c:forEach>标签循环取出数据,所以需要先导入JSTL标签库 <%@ tag ...

  7. 进度条(progress_bar)

    环境:linux.centos6.5 #include<stdio.h> #include<unistd.h> int main() { ]={'\0'}; char ch[] ...

  8. 在Azure DevOps Server (TFS 2019) 流水线传递参数

    变量概述 在Azure DevOps Server的流水线中,变量是衔接不同任务和不通代理之间的桥梁,它可以使相对松散.各自独立的任务之间相关影响并共享数据.在流水线中使用变量,可以在各任务之间相互调 ...

  9. LOJ#6387 「THUPC2018」绿绿与串串 / String (Manacher || hash+二分)

    题目描述 绿绿和 Yazid 是好朋友.他们在一起做串串游戏. 我们定义翻转的操作:把一个串以最后一个字符作对称轴进行翻转复制.形式化地描述就是,如果他翻转的串为 RRR,那么他会将前 ∣R∣−1个字 ...

  10. Go语言数据类型

    目录 基本数据类型说明 整型 浮点型 字符 字符类型本质探讨 布尔型 字符串 指针 值类型与引用类型 基本数据类型默认值 基本数据类型相互转换 注意事项 其他基本类型转string类型 string类 ...