基本概念
- 数据是描述客观事物的数值、字符以及能输入机器且能被处理的各种符号集合。数据的含义非常广泛,除了通常的数值数据、字符、字符串是数据以外,声音、图像等一切可以输入计算机并能被处理的都是数据。例如除了表示人的姓名、身高、体重等的字符、数字是数据,人的照片、指纹、三维模型、语音指令等也都是数据。
- 数据元素是数据的基本单位,是数据集合的个体,在计算机程序中通常作为一个整体来进行处理。例如一条描述一位学生的完整信息的数据记录就是一个数据元素;空间中一点的三维坐标也可以是一个数据元素。数据元素通常由若干个数据项组成,例如描述学生相关信息的姓名、性别、学号等都是数据项;三维坐标中的每一维坐标值也是数据项。数据项具有原子性,是不可分割的最小单位。
- 数据对象是性质相同的数据元素的集合,是数据的子集。例如一个学校的所有学生的集合就是数据对象,空间中所有点的集合也是数据对象。
- 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。是组织并存储数据以便能够有效使用的一种专门格式,它用来反映一个数据的内部构成,即一个数据由那些成分数据构成,以什么方式构成,呈什么结构。
由于信息可以存在于逻辑思维领域,也可以存在于计算机世界,因此作为信息载体的数据同样存在于两个世界中。表示一组数据元素及其相互关系的数据结构同样也有两种不同的表现形式,一种是数据结构的逻辑层面,即数据的逻辑结构;一种是存在于计算机世界的物理层面,即数据的存储结构。
数据的逻辑结构按照数据元素之间相互关系的特性来分,可以分为以下四种结构:集合、线性结构、树形结构和图状结构。这里讨论的数据结构主要有线性表、栈、队列、树,其中线性表、栈、队列属于线性结构,树和图属于非线性结构。
数据的存储结构主要包括数据元素本身的存储以及数据元素之间关系表示。通过数据元素的定义可以看出,我们可以很容易的使用 Java 中的一个类来实现它,数据元素的数据项就是类的成员变量。
数据元素之间的关系在计算机中主要有两种不同的表示方法:顺序映像和非顺序映像,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。顺序存储结构的特点是:数据元素的存储对应于一块连续的存储空间,数据元素之间的前驱和后续关系通过数据元素在存储器中的相对位置来反映。链式存储结构的特点是:数据元素的存储对应的是不连续的存储空间,每个存储节点对应一个需要存储的数据元素。元素之间的逻辑关系通过存储节点之间的链接关系反映出来。
由于我们是在 Java 这种计算机高级程序设计语言的基础上来讨论数据结构,因此,我们在讨论数据的存储结构时不会在真正的物理地址的基础上去讨论顺序存储和链式存储,而是在 Java 语言提供的一维数组以及对象的引用的基础上去讨论和实现数据的存储结构。
线性表
- 线性表定义
线性表(linear list)是n个类型相同数据元素的有限序列,通常记作(a0, a1, …ai-1, ai, ai+1 …, an-1 )。
在这里特别需要注意的是线性表和数组的区别。从概念上来看,线性表是一种抽象数据类型;数组是一种具体的数据结构。线性表与数组的逻辑结构是不一样的,线性表是元素之间具有1对1的线性关系的数据元素的集合,而数组是一组数据元素到数组下标的一一映射。并且从物理性质来看,数组中相邻的元素是连续地存储在内存中的;线性表只是一个抽象的数学结构,并不具有具体的物理形式,线性表需要通过其它有具体物理形式的数据结构来实现。在线性表的具体实现中,表中相邻的元素不一定存储在连续的内存空间中,除非表是用数组来实现的。对于数组,可以利用其下标在一个操作内随机存取任意位置上的元素;对于线性表,只能根据当前元素找到其前驱或后继,因此要存取序号为i的元素,一般不能在一个操作内实现,除非表是用数组实现的。
线性表是一种非常灵活的数据结构,线性表可以完成对表中数据元素的访问、添加、删除等操作,表的长度也可以随着数据元素的添加和删除而变化。 - 线性表的顺序存储与实现
线性表的顺序存储是用一组地址连续的存储单元依次存储线性表的数据元素。假设线性表的每个数据元素需占用K个存储单元,并以元素所占的第一个存储单元的地址作为数据元素的存储地址。则线性表中序号为i的数据元素的存储地址LOC(ai)与序号为i+1 的数据元素的存储地址LOC(ai+1)之间的关系为
LOC(ai+1) = LOC(ai) + K
通常来说,线性表的i号元素ai的存储地址为
LOC(ai) = LOC(a0) + i×K
其中LOC(a0)为 0 号元素a0的存储地址,通常称为线性表的起始地址。
线性表的这种机内表示称作线性表的顺序存储。它的特点是,以数据元素在机内存储地址相邻来表示线性表中数据元素之间的逻辑关系。每一个数据元素的存储地址都和线性表的起始地址相差一个与数据元素在线性表中的序号成正比的常数。由此,只要确定了线性表的起始地址,线性表中的任何一个数据元素都可以随机存取,因此线性表的顺序存储结构是一种随机的存储结构。
图1
由于高级语言中的数组具也有随机存储的特性,因此在抽象数据类型的实现中都是使用数组来描述数据结构的顺序存储结构。通过图1,我们看到线性表中的数据元素在依次存放到数组中的时候,线性表中序号为 i 的数据元素对应的数组下标也为 i,即数据元素在线性表中的序号与数据元素在数组中的下标相同。
在这里需要注意的是,如果线性表中的数据元素是对象时,数组存放的是对象的引用,即线性表中所有数据元素的对象引用是存放在一组连续的地址空间中。
图2
由于线性表的长度可变,不同的问题所需的最大长度不同,那么在线性表的具体实现中我们是使用动态扩展数组大小的方式来完成线性表长度的不同要求的。
代码 线性表的数组实现
public class ListArray implements List {
private final int LEN = 8; //数组的默认大小
private Strategy strategy; //数据元素比较策略
private int size; //线性表中数据元素的个数
private Object[] elements; //数据元素数组
//构造方法
public ListArray () {
this(new DefaultStrategy());
}
public ListArray (Strategy strategy){
this.strategy = strategy;
size = 0;
elements = new Object[LEN];
}
//返回线性表的大小,即数据元素的个数。
public int getSize() {
return size;
}
//如果线性表为空返回 true,否则返回 false。
public boolean isEmpty() {
return size==0;
}
//判断线性表是否包含数据元素 e
public boolean contains(Object e) {
for (int i=0; i<size; i++)
if (strategy.equal(e,elements[i])) return true;
return false;
}
//返回数据元素 e 在线性表中的序号
public int indexOf(Object e) {
for (int i=0; i<size; i++)
if (strategy.equal(e,elements[i])) return i;
return -1;
}
//将数据元素 e 插入到线性表中 i 号位置
public void insert(int i, Object e) throws OutOfBoundaryException {
if (i<0||i>size)
throw new OutOfBoundaryException("错误,指定的插入序号越界。");
if (size >= elements.length)
expandSpace();
for (int j=size; j>i; j--)
elements[j] = elements[j-1];
elements[i] = e; size++;
return;
}
private void expandSpace(){
Object[] a = new Object[elements.length*2];
for (int i=0; i<elements.length; i++)
a[i] = elements[i];
elements = a;
}
//将数据元素 e 插入到元素 obj 之前
public boolean insertBefore(Object obj, Object e) {
int i = indexOf(obj);
if (i<0) return false;
insert(i,e);
return true;
}
//将数据元素 e 插入到元素 obj 之后
public boolean insertAfter(Object obj, Object e) {
int i = indexOf(obj);
if (i<0) return false;
insert(i+1,e);
return true;
}
//删除线性表中序号为 i 的元素,并返回之
public Object remove(int i) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的删除序号越界。");
Object obj = elements[i];
for (int j=i; j<size-1; j++)
elements[j] = elements[j+1];
elements[--size] = null;
return obj;
}
//删除线性表中第一个与 e 相同的元素
public boolean remove(Object e) {
int i = indexOf(e);
if (i<0) return false;
remove(i);
return true;
}
//替换线性表中序号为 i 的数据元素为 e,返回原数据元素
public Object replace(int i, Object e) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的序号越界。");
Object obj = elements[i];
elements[i] = e;
return obj;
}
//返回线性表中序号为 i 的数据元素
public Object get(int i) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的序号越界。");
return elements[i];
}
}
- 线性表的链式存储与实现
实现线性表的另一种方法是链式存储,即用指针将存储线性表中数据元素的那些单元依次串联在一起。这种方法避免了在数组中用连续的单元存储元素的缺点,因而在执行插入或删除运算时,不再需要移动元素来腾出空间或填补空缺。然而我们为此付出的代价是,需要在每个单元中设置指针来表示表中元素之间的逻辑关系,因而增加了额外的存储空间的开销。
3.1 单链表
链表是一系列的存储数据元素的单元通过指针串接起来形成的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针。这里具有一个数据域和多个指针域的存储单元通常称为结点(node)。
图3
代码 单链表结点定义
public class SLNode implements Node {
private Object element;
private SLNode next;
public SLNode() {
this(null,null);
}
public SLNode(Object ele, SLNode next){
this.element = ele;
this.next = next;
}
public SLNode getNext(){
return next;
}
public void setNext(SLNode next){
this.next = next;
}
/**************** Methods of Node Interface **************/
public Object getData() {
return element;
}
public void setData(Object obj) {
element = obj;
}
}
单链表是通过上述定义的结点使用 next 域依次串联在一起而形成的。
图4
链表的第一个结点和最后一个结点,分别称为链表的首结点和尾结点。尾结点的特征是其 next 引用为空(null)。链表中每个结点的 next 引用都相当于一个指针,指向另一个结点,借助这些 next 引用,我们可以从链表的首结点移动到尾结点。如此定义的结点称为单链表(single linked list)。在单链表中通常使用 head 引用来指向链表的首结点,由 head 引用可以完成对整个链表中所有节点的访问。有时也可以根据需要使用指向尾结点的 tail 引用来方便某些操作的实现。
在单链表结构中还需要注意的一点是,由于每个结点的数据域都是一个 Object 类的对象,因此,每个数据元素并非真正如图 3-4 中那样,而是在结点中的数据域通过一个 Object类的对象引用来指向数据元素的。
与数组类似,单链表中的结点也具有一个线性次序,即如果结点 P 的 next 引用指向结点 S,则 P 就是 S 的直接前驱,S 是 P 的直接后续。单链表的一个重要特性就是只能通过前驱结点找到后续结点,而无法从后续结点找到前驱结点。在单链表中通常需要完成数据元素的查找、插入、删除等操作。下面我们逐一讨论这些操作的实现。
在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的 next 引用来一次访问链表中的每个结点以完成相应的查找操作。例如需要在单链表中查找是否包含某个数据元素 e,则方法是使用一个循环变量 p,起始时从单链表的头结点开始,每次循环判断 p所指结点的数据域是否和 e 相同,如果相同则可以返回 true,否则继续循环直到链表中所有结点均被访问,此时 p 为 null。该过程如图5所示。
图5
在单链表中数据元素的插入,是通过在链表中插入数据元素所属的结点来完成的。对于链表的不同位置,插入的过程会有细微的差别。图 6(a)、6(b)、6(c)分别说明了在单链表的表头、表尾以及链表中间插入结点的过程。
图6
类似的,在单链表中数据元素的删除也是通过结点的删除来完成的。在链表的不同位置删除结点,其操作过程也会有一些差别。图 7(a)、7(b)、7(c)分别说明了在单链表的表头、表尾以及链表中间删除结点的过程。
图7
3.2 双向链表
单链表的一个优点是结构简单,但是它也有一个缺点,即在单链表中只能通过一个结点的引用访问其后续结点,而无法直接访问其前驱结点,要在单链表中找到某个结点的前驱结点,必须从链表的首结点出发依次向后寻找,但是需要Ο(n)时间。为此我们可以扩展单链表的结点结构,使得通过一个结点的引用,不但能够访问其后续结点,也可以方便的访问其前驱结点。扩展单链表结点结构的方法是,在单链表结点结构中新增加一个域,该域用于指向结点的直接前驱结点。扩展后的结点结构是构成双向链表的结点结构,如图 8 所示。
图8
代码 双向链表结点定义
public class DLNode implements Node {
private Object element;
private DLNode pre;
private DLNode next;
public DLNode() {
this(null,null,null);
}
public DLNode(Object ele, DLNode pre, DLNode next){
this.element = ele;
this.pre = pre;
this.next = next;
}
public DLNode getNext(){
return next;
}
public void setNext(DLNode next){
t his.next = next;
}
public DLNode getPre(){
return pre;
}
public void setPre(DLNode pre){
this.pre = pre;
}
/****************Node Interface Method**************/
public Object getData() {
return element;
}
public void setData(Object obj) {
element = obj;
}
}
单链表的插入操作,除了首结点之外必须在某个已知结点后面进行,而在双向链表中插入操作在一个已知的结点之前或之后都可以进行。例如在某个结点 p 之前插入一个新结点的过程如图 9所示。
图9
单链表的删除操作,除了首结点之外必须在知道待删结点的前驱结点的基础上才能进行,而在双向链表中在已知某个结点引用的前提下,可以完成该结点自身的删除。图 10表示了删除 p 的过程。
图10
3.3 线性表的单链表实现
代码 线性表的单链表实现
public class ListSLinked implements List {
private Strategy strategy; //数据元素比较策略
private SLNode head; //单链表首结点引用
private int size; //线性表中数据元素的个数
public ListSLinked () {
this(new DefaultStrategy());
}
public ListSLinked (Strategy strategy) {
this.strategy = strategy;
head = new SLNode();
size = 0;
}
//辅助方法:获取数据元素 e 所在结点的前驱结点
private SLNode getPreNode(Object e){
SLNode p = head;
while (p.getNext()!=null)
if (strategy.equal(p.getNext().getData(),e)) return p;
else p = p.getNext();
return null;
}
//辅助方法:获取序号为 0<=i<size 的元素所在结点的前驱结点
private SLNode getPreNode(int i){
SLNode p = head;
for (; i>0; i--) p = p.getNext();
return p;
}
//获取序号为 0<=i<size 的元素所在结点
private SLNode getNode(int i){
SLNode p = head.getNext();
for (; i>0; i--) p = p.getNext();
return p;
}
//返回线性表的大小,即数据元素的个数。
public int getSize() {
return size;
}
//如果线性表为空返回 true,否则返回 false。
public boolean isEmpty() {
return size==0;
}
//判断线性表是否包含数据元素 e
public boolean contains(Object e) {
SLNode p = head.getNext();
while (p!=null)
if (strategy.equal(p.getData(),e)) return true;
else p = p.getNext();
return false;
}
//返回数据元素 e 在线性表中的序号
public int indexOf(Object e) {
SLNode p = head.getNext();
int index = 0;
while (p!=null)
if (strategy.equal(p.getData(),e)) return index;
else {index++; p = p.getNext();}
return -1;
}
//将数据元素 e 插入到线性表中 i 号位置
public void insert(int i, Object e) throws OutOfBoundaryException {
if (i<0||i>size)
throw new OutOfBoundaryException("错误,指定的插入序号越界。");
SLNode p = getPreNode(i);
SLNode q = new SLNode(e,p.getNext());
p.setNext(q);
size++;
return;
}
//将数据元素 e 插入到元素 obj 之前
public boolean insertBefore(Object obj, Object e) {
SLNode p = getPreNode(obj);
if (p!=null){
SLNode q = new SLNode(e,p.getNext());
p.setNext(q);
size++;
return true;
}
return false;
}
//将数据元素 e 插入到元素 obj 之后
public boolean insertAfter(Object obj, Object e) {
SLNode p = head.getNext();
while (p!=null)
if (strategy.equal(p.getData(),obj)){
SLNode q = new SLNode(e,p.getNext());
p.setNext(q);
size++;
return true;
}
else p = p.getNext();
return false;
}
//删除线性表中序号为 i 的元素,并返回之
public Object remove(int i) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的删除序号越界。");
SLNode p = getPreNode(i);
Object obj = p.getNext().getData();
p.setNext(p.getNext().getNext());
size--;
return obj;
}
//删除线性表中第一个与 e 相同的元素
public boolean remove(Object e) {
SLNode p = getPreNode(e);
if (p!=null){
p.setNext(p.getNext().getNext());
size--;
return true;
}
return false;
}
//替换线性表中序号为 i 的数据元素为 e,返回原数据元素
public Object replace(int i, Object e) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的序号越界。");
SLNode p = getNode(i);
Object obj = p.getData();
p.setData(e);
return obj;
}
//返回线性表中序号为 i 的数据元素
public Object get(int i) throws OutOfBoundaryException {
if (i<0||i>=size)
throw new OutOfBoundaryException("错误,指定的序号越界。");
SLNode p = getNode(i);
return p.getData();
}
}
TO BE CONTINUE…