本文讨论三种最简单的数据结构,也是抽象数据类型(ADT)的最基本的例子:表、栈和队列。
ADT即带有一组操作的一些对象的集合。诸如表、集合、图以及它们与各自的操作一起形成的这些对象都可以被看做是ADT。
ADT的定义中并没有对其实现有任何提起,Java类虽然考虑到了ADT的实现,但也隐藏了实现的细节。对于每种ADT并不存在什么法则来约束他们必须要有哪些操作。我们下面详细讲述的三种ADT都有多种实现方法,但一旦它们被正确实现后,应用时却不必知道他们是如何实现的。
这些表述比较抽象,我们在后面学习中逐步介绍。
首先,约定我们处理形如A(0),A(1),A(2)···,A(N-1)的一般的表。
表的实现方式一般有数组、链表。
(1)数组 _ Array
某些情况下,表是通过在高端进行插入操作建成的,其后只发生对数组的访问,此时,数组是表的一种恰当实现方式。
常用操作复杂度分析:
为了克服插入和删除的高复杂度,我们介绍另一种数据结构:链表
(2)线性链表 _ Linked List
线性链表由一系列节点组成,这些节点不必在内存中相连从而避免了插入删除时每个部分可能的整体移动带来的线性开销。每一个节点均含有表元素和到包含该元素后继元素的节点的next链。
常用操作复杂度分析:
(3)双向链表 _ Doubly Linked List
即在线性链表基础上,在每个节点增加一个指向前驱节点的链。
Collections API位于java.util包中,是Java类库中集合类的基本接口,包含了一些普通数据结构的实现。表ADT是在Collections API中实现的数据结构之一。
(1)有关接口
接口不能用new来实例化一个接口,既不能够造接口对象,但能声明接口变量,且接口变量必须引用实现了接口的类变量。
(2)Collection接口
集合的概念在Collection接口中得到抽象,用来存储一组类型相同的对象。
Collection接口扩展了Iterable接口。
该接口的一些最重要的部分见下图
(3)Iterator接口
实现Iterable接口的集合必须提供一个称为iterator的方法,该方法返回一个iterator类型的对象。
Iterator接口的思路是,通过Iterator方法,每个集合均可创建并返回给用户一个实现了Iterator接口的对象,并将当前位置的概念在对象内部存储起来。
(4)List接口
我们要讲的List是由java.util包中的List接口指定的,List接口继承了Collection接口。
该接口的一些最重要的部分见下图
(5)ListIterator接口
ListIterator接口扩展了List的Iterator的功能
该接口的一些最重要的部分见下图
主流的实现List ADT的方式有:ArrayList类提供的List ADT的一种可增长数组的实现方式,LinkedList类提供的List ADT的双链表实现。
(1)ArrayList类的实现
我们提出了一个便于使用的ArrayList泛型类的实现,详见之前的博文ArrayList泛型类的实现实战
注意这并不是库中的ArrayList类的标准实现,只是可以使用的一个例子。
注意size()是集合的方法,用来返回集合中对象的数量,length()是数组中的方法,用来返回数组的长度。从而理清程序中行58。
(2)LinkedList类的实现
我们提出了一个便于使用的LinkedList泛型类的实现,详见之前的博文LinkedList泛型类的实现实战
注意这并不是库中的LinkedList类的标准实现,只是可以使用的一个例子。
栈(Stack)是插入和删除操作被限制在表的末端的表,也叫作后进先出(LIFO)表。表的末端叫做栈的顶,栈顶的元素也是表的唯一的可见元素。
前面说的基本操作插入对应push(进栈),删除对应pop(出栈)。
由于栈是一个表,因此任何实现表的方法都能实现栈。
(1)ArrayList和LinkedList都支持占操作,而且还是上优的选择
(2)有些特殊情况,也可以通过简化ArrayList和LinkedList中的逻辑,使用链表结构或者使用数组实现栈。
栈的链表实现
即用单链表实现,很简单。
栈的数组实现
模仿了ArrayList中的add操作,因此实现方法非常简单。与每个栈相关联的操作是theArray和topOfStack,将x进栈是topOfStack增1然后置theArray[topOfStack]=x,出栈则是返回theArray[topOfStack],然后topOfStack增减1。
另外值得注意的是,上述操作不仅在常数时间内完成,而且还超快,这得益于现代计算机往往将栈作为它的指令系统的一部分,可以说,栈很可能是在计算机科学中在数组之后的最基本的数据结构了。
(1)编译器的平衡符号检测
基本流程图如下
此算法复杂度是线性的,只需对输入进行一趟检验即可
(2)后缀表达式
对于后缀或者逆波兰记法的表达式,没有必要知道任何优先的规则,通过使用栈的算法计算,复杂度是O(N)。
而一般我们是用的普通表达式,也称作中缀表达式,可以通过栈转换成上面提到的后缀表达式,这个过程复杂度也为O(N)。
例如中缀表达式 a+b*c+(d*e+f)*g
可以转换成为后缀表达式 abc*+de*f+g*+
。
(3)方法调用
当调用一个新方法时,主调例程的所有局部变量、当前位置将由系统保存,指派给机器的寄存器,以便在新方法运行后知道向哪里转移。
当涉及到递归时,其实方法调用和方法返回基本上类似于前面提到的开括号和闭括号。
方法调用和方法返回的过程可以用栈来完成,前面提到的所存储的信息称为活动记录或者栈帧。
有关栈和递归的进一步介绍可以见此博文数据结构复习篇:用栈实现递归
此外,关于尾递归:尾递归即涉及在最后一行的递归调用,尾递归可以通过将代码放到一个while循环中并用每个方法参数的一次赋值代替递归调用而被手工消除。
和栈一样,队列也是表,不同的是,使用队列时插入在一端进行,而删除则在另一端进行。
类似的,队列的基本操作是enqueue(入队)和dequeue(出对),前者在队尾(rear)插入一个元素,后者在队头(front)删除一个元素。
类似于栈,对于队列而言任何的表的实现都是合法的,且对于每一种操作,链表实现和数组实现都给出O(1)运行时间。
没有太特别的,暂不介绍了