前面说了线性表的顺序存储结构,它最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。
那我们能不能针对这个缺陷或者说遗憾提出解决的方法呢?要解决这个问题,我们就得考虑一下导致这个问题的原因!
为什么当插入和删除时,就要移动大量的元素?
原因就在于相邻两元素的存储位置,它们在内存中的位置连续的,中间没有间隙,当然就无法快速插入和删除。
那么如何解决这个问题,就有了我们今天的主题——线性表的链式存储结构
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。
比起顺序存储结构每个数据元素只需要存储一个位置就可以了。现在链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址(指针)。
也就是说除了存储其本身的信息外,还需存储一个指示其直接后继的存储位置的信息。
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,称为结点(Node)。
n个结点链接成一个链表,即为线性表(a1, a2, a3, …, an)的链式存储结构。
因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
头结点与头指针
头结点是指链表中的第一个结点,有真实头结点和虚拟头结点之分
真实头结点: 第一个结点存储数据,也存储下一个结点的地址
虚拟头结点: 第一个结点不存数据,也存储下一个结点的地址
那有人就疑惑了,既然头结点的数据域不存储任何信息,那么头指针和头结点又有何异同呢?
头指针
-
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
无论链表是否为空,头指针均不为空。
头指针是链表的必要元素。
头结点
-
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。
有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。
头结点不一定是链表的必须要素。
最后有头指针肯定有尾指针,而尾指针同头指针,不过是链表中最后一个结点的指针而已
线性表
介绍完链表,就是具体的实现(本此实现采用虚拟头结点)。在顺序存储结构中有线性表,是用数组的实现的,现在已经知道了链表的内容,这里我们可以用链表实现一个线性表。具体方法如类图:
首先,看是如何插入元素
头插法动图如下
尾插法动图如下
头插尾插结合动图如下
一般插入动图如下
代码实现
@Override
public void add(int index, E e) {
if(index<0||index>size){
throw new IllegalArgumentException("插入角标非法!");
}
Node n=new Node(e,null);
if(index==0){
//头插
n.next=head.next;
head.next=n;
if(size==0){
rear=n;
}
}else if(index==size){
//尾插
rear.next=n;
rear=rear.next;
}else{
//一般插入
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
n.next=p.next;
p.next=n;
}
size++;
}
如何删除元素
删除头部动图如下
删除尾部动图如下
一般删除动图如下
代码实现
@Override
public E remove(int index) {
if(index<0||index>=size){
throw new IllegalArgumentException("删除角标非法!");
}
E res=null;
if(index==0){
//头删
Node p=head.next;
res=p.data;
head.next=p.next;
p.next=null