前言
线性结构是非常简单且常用的数据结构,而线性表则是一种非常典型的线性结构。
本节代码传送门,欢迎star:https://github.com/mcrwayfun/java-data-structure
1. 线性表的定义
通常将线性表定义为n个元素的有限的序列,可以表为为
其中L为表名,a是表中不可再分割的原子数据,亦称为结点或者表项。n是表中表项的个数,也被称为表的长度。当n=0时叫做空表。
线性表的的第一个表项称为表头,最后一个项称为表尾。因为线性表是有序的,这就意味着每个相邻的表项之间都有直接前驱和直接后继的关系,也就是说,除了表头没有直接前驱,其他表项有且仅有一个直接前驱;除了表尾没有直接后继,其他表项有且仅有一个直接后继。
2. 线性表的数据结构
在类库中,java语言包含了一些普通数据结构的实现。该语言的这一部分通常叫做Collections API,表ADT是在Collections API中实现的数据结构之一,看一下其基本的方法:
public interface Collection<AnyType> extends Iterable<AnyType>
{
// 返回表中元素个数
int size();
// 判断表是否为空
boolean isEmpty();
// 清空一张表的数据
void clear();
// 判断表是否含有数据项x
boolean contains(AnyType x);
// 向表中添加一个新的元素x
boolean add(AnyType x);
// 移除表中的元素x
boolean remove(AnyType x);
}
3. 链表
链表可以分为有序线性表和无序线性表,本小节仅考虑无序线性表,即链表。链表又分为单链表,环形链表和双向链表。下面将围绕接口Collection中的基本方法实现链表以及完成一些拓展。
3.1 单链表
为了克服顺序表的缺点(新增或删除元素开销大;需要事先分配连续的存储空间),采用链接方式来存储线性表,通常将链接方式存储的线性表成为链表。链表适用于插入或者删除频繁,存储空间需求不定的情形。
3.1.1 单链表的定义
单链表的存储结点包含两部分,一个是数据域(data),用于存储线性表的一个数据元素;一个是指针域(link),用于存放一个指针,该指针指向链表中下一个结点的开始存储地址。用图表示为:
用API可以表示为,其中item为数据域,next为指针域:
class Node<E> {
E item;
Node<E> next;
}
一个线性表的单链表结构可以用下图表示:
其中,单链表的表头可以通过头指针first找到,其他结点的地址则在前驱结点的link域中,表尾结点没有后继,其link域中存放了一个空指针NULL作为终结。因此,要访问单链表中的任一结点,都需要根据头指针first找到第一个结点,再按照各结点link域中存放的指针顺序往下寻找。因此,操作单链表最重要的便是维护一个头指针first
3.1.2 单链表插入和删除
利用单链表来表示线性表,使得插入和删除操作变得非常方便,只需要改变链表结点中的指针值,无需移动表中的元素,就能高效的完成插入和删除操作。现有单链表
首先来看下单链表的ADT,MyList是一个接口,定义了线性表的基本接口,会放在源码中,这里不作展示
public class SinglyLinkedList<E> implements MyList<E> {
// 表中元素个数
int size = 0;
// 表头指针
Node<E> first;
// 存储结点
private static class Node<E> {
E item;
Node<E> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
}
}
插入操作
对于插入算法,需要在数据项ai结点后插入一个新的元素x,那么可能会出现3种情况:
(1) 在表的表头插入一个新结点,那么新结点成为表头,并将first指针指向新结点
// 新结点链向原表头
newNode.next = first;
// 新结点成为表头
first = newNode;
(2) 若ai是链表的最后一个结点,则新结点应追加在表尾。那么新元素成为表尾,且指针域为NULL。定义检测指针cur,需要遍历链表,找到原来的表尾an,新结点则在表尾后插入
// 循环找到表尾last,last指针域指向新结点
cur.next = newNode;
(3) 若ai即不是表头,也不是表尾。此时,首先让一个检测指针cur指向ai所在的结点,新结点插入到ai所在结点后
// 新结点指针域指向ai的下一个结点