前边我们通过使用数组实现了队列结构。在使用数组的队列中,数组在空间中的地址是连续的,可以通过数组的下标直接访问获得该数组中的元素。在这里我们通过另一种方式来实现----链表。
1.什么是链表。
那究竟什么是链表呢?我们来看一下定义:
链表的定义:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表是由一个个的节点所组成的。节点是链表独有的组成元素。而节点中又包含了信息,在节点中包含了数据和指向下一个元素的指针.一个个的节点通过指针连成一条链状结构的表。,形成链表。
简单的来说,链表就像是一条链子。由一个一个的小环构成最后连接成一个长的链子。而节点就可以看做是组成链子的一个个的环,环上携带的信息(数据域用来存放数据,指针域用来存放下一个节点的地址)。
在明白了什么是链表,那链表有什么用处呢?
最常见的也是最容易理解的链条,由于链条是由一个个的环之间互相的连着,链条在生活中的应用比较广泛,前一个链子与后一个链子之间相互联系,如果将链子的环想象成比较大的,那么就和每个环的位置没有关系了,前一个环中连接着下一个环。(就在链表前一个节点的指针域指向下一个节点的数据域)
在这里要注意,在java中的链表和实际的链有区别,链子有许多的环组成,而链表也是由许多的同类型的数据元素组成的。但在java中链表中的数据不仅仅局限于整数int,小数double,布尔boolean和字符串String等等简单的数据类型。只要是同一类型的数据都可以使用链表来储存。
动态链表
我们通过使用数组实现了线性表。在这里,我们要通过动态数组实现线性表。
链表和线性表一样也是有增删改查的基本操作。
我们会发现,如果用动态数组实现线性表,那么元素在数组中存储的话是地址连续的,因为数组中的存储空间是连续的。但有时候内存中并没有一定大小的内存空间那该怎弄办呢?
于是便有了单链表(只能从一个方向到另一个方向)
如果用动态链表实现线性表的话,那么元素的地址是随机的,因为节点对象创建时的地址是由系统底层决定的且随机的,所以为了保持线性表的性质,每一个节点除了存储数据信息外,还需要存储其下一个节点的地址
动态数组实现线性表也好,还是动态链表实现也罢,它们对线性表的操作都是增删改查,我们可以先对两者的具体实现定义统一的操作规范,对他们共有的部分做一个接口:
* List是线性表的最终父接口
* */
public interface List<E> {
public int getSize();// 获取表中元素的个数(线性表的长度)
public boolean isEmpty();// 判断表是否为空
public void add(int index,E e);//在表中指定的index角标处添加元素e
public void addFirst(E e);//在表的表头位置插入一个元素
public void addLast(E e);//在表的表尾位置插入一个元素
public E get(int index);//在表中获取指定index角标处的元素
public E getFirst();//获取表中表头的元素
public E getLast();//获性表中表尾的元素
public void set(int index,E e);// 修改表中指定index处的元素为新元素e
public boolean contains(E e);//判断表中是否包含指定元素e 默认从前往后找
public int find(E e);//在表中获取指定元素e的角标 默认从前往后找
public E remove(int index);//在表中删除指定角标处的元素 并返回
public E removeFirst();//删除表中的表头元素
public E removeLast();//删除表中的表尾元素
public void removeElement(E e);//在表中删除指定元素
public void clear();清空表
}
既然是一个链表。那么它就应该有头和尾。
我们把链表的第一个节点叫作头结点,把最后一个节点叫作尾结点, 但结点也有真假之分,为什么呢?
我们相一下,如果一个链表有5个节点的话(如上图)那第一个节点是头结点,最后一个是尾节点。那么试想一下,如果一个链表中节点数量为0,那头结点和尾节点在哪里呢?
是不是没有头结点呢? 这里就引入了虚拟头结点,我们就假设每一个链表都有一个虚拟头结点,但是这个虚拟头结点不存放元素。当没有结点时但虚拟头结点依然存在。
真实头结点的头结点是存放有数据的
虚拟头结点的头结点是存放有数据的(一下说明为虚拟头结点)
我们既然将链表的 头尾节点都找到了。那么就可说明链表的状态了:对于一个链表来说要么他有多少个节点,它就有多长,我们通过量表的特性来表示链表的状态;链表是由前一个指向后一个,如果一个链表的某个节点的下一个没有了,那该链表就结束、;但是当一个链表为空,也就是链表没有下一个节点。但头结点本身不存数据为空,那么该链表为空。
为了能更好的表示链表中的节点,引入了指针:但指针仅仅是一个引用变量存储结点地址的指针。用来更好的表示节点。
我们经常用指针来更好的表示节点,那么就是头结点就可以用头指针来表示。尾节点就用尾指针表示。
我们了解了链表,我们说过,它与数组有相同的操作,无非是增删改查, 接下来我们看一看链表的基本操作。
链表的基本操作
链表和数组类似,也有增删改查的操作。我们先看一看增加操作。
我以铁链作为例子:假设一定长度的链子,我觉的有点短该怎么办,那肯定是在增加环的数量了,那么问题来了,我们该如何增加,我们都知道链子是一环扣一环的,如果要增加环,我们做容易想到末尾在扣一个环,但我们想一想,一个链是所有环相扣,我们除了在为,也可在头部扣上,甚至在中间的任意为值扣上一个环。 那么链表也是一样的。
我们往一个链表中添加元素。可以在最末尾添加,也可以在头部添加,或在中间位置任意添加。
所以链表也分为 头插和尾插还有中间指定位置的插入。
一般情况下有以下三种情况
尾插法:将新元素的地址赋给尾指针,尾指针向后移动
头插法:(将元素从头部插入)将链表中的第一个有效节点元素的地址赋给插入的新元素,头指针向前移动
我们先假设一个空链表头指针等于尾指针且长度为0;且头指针的下一个为空:
上图为空链表,现在要插入一个数据,首先,我们要,明白在链表中是由一个个节点所组成,前面我们说过节点是由数据域和指针与组成。所以我们要将数据包装成一个节点,然后对结点进行操作。
我们将结点看成一个对象,将数据包装成接点。
E data; //数据域
Node next; //指针域
public Node(){
this(null,null);
}
public Node(E data,Node next){
this.data=data;
this.next=next;
}
每次插入一个数据就对这个数据进行包装成一个节点,对结点进行操作。然后进行插入操作。
当一个空的链表中进入一个节点,这个精包装的节点中只有数据域,没有指针域。使用头插法。将头指针的下一个给进入结点的下一个。将进入元素的地址给头指针,这个时候进入元素的下一个指向空。如下图,我们发现当链表为空的情况下,无论是头插,还是尾插,尾指针都要向后移动。
当在进入一个元素时。使用头插法时:将头结点的下一个给新元素,将新元素的的地址个头结点,这样就会发现尾指针一直都在最后一个节点。头指针也没有移动但是链表的长度一直在增加。也达到了我们要插入元素的需求。
链表的实现
我们在前面说了,无论是线性表还是链表都实现了list接口,所以我们的链表也应该实现List接口:
`
public class LinkedList implements List {
}
我在节点的地方说过,链表有节点组成,无论是删除还是增加都要将数据包装成结点。之后对结点进行操作。
首先我们要创建头尾结点,还要将链表的长度标是出来。
private Node head; //指向虚拟头结点的头指针
private Node rear; //指向尾结点的尾指针
private int size; //记录元素的个数
接下来是构造函数:
head=new Node();
rear=head;
size=0;
}
public LinkedList(E[] arr){
head=new Node();
rear=head;
size=0;
}
这里通过构造函数够构造一个空的链表,在该链表中没有结点,所以链表长度为0,也可以通过传入数组进行包装,将结点中的数据域变成一个数组存放。
接下来就该实现接口中的方法了;
getSize() 方法:
长度其实就是szie的值:获取size的值。然后通过返回。
public int getSize() {
return size;
}
判空方法isEmpty()方法
public boolean isEmpty() {
return size==0&&head.next==null;
}
在判断一个链表是否为空,我们只需要知道该链表的长度。如果该链表的长度为0,也就说明该链表中没有有效节点,也就是没有元素。则该链表为空。如果不放心,也可以让头结点的下一个位置为空。也可以证明该链表为空,没有任何结点。
add(int index, E e) :
这个问题我们前面分析过,插入元素有三种情况,分别是头插,尾插和一般的中间插。
在插入的时候,我们忽略了几个问题,那就是,链表需要扩容吗?
我们知道链表是由一个个结点组成。这么说吧,你要在链子的最后的加上一个环,那么链表就算是自动扩容了,所以链表不存在扩容。
其次你要插入元素根据角标插入,首先判断传入角标是否合法,如果不合法那就抛出异常。
if(index<0||index>size){
throw new IllegalArgumentException("插入角标非法!");
}
如果合法就进行插入,插入有三种情况:
在表头插入:index=0
在表中插入:index∈(0,size)任意
在表尾插入:index=size
Node n=new Node(e,null);
if(index==0){ //头插
n.next=head.next;
head.next=n;
if(size==0){
rear=n;
}
首先将传入的元素包装成一个节点,根据传入的下标,如果在表头插入该元素在表的头部,要让该元素的下一个变为头指针的下一个,而头指针的下一个就是该元素,最后这个表的长队增加。如果为空表的话,那尾指针指向新结点。
当你要插入的结点的为位置在最后一个那就是尾插:尾插主要是尾指针向后移动,尾指针的下一个为空,最后链表长度加1;
rear.next=n;
rear=rear.next;
一般的插法:首先定义一个临时结点,让这个临时结点从头结点开始,按照角标依次向后查找直到临时结点的下一个为所要查的位置。将改位置给进来的结点。将原来这个位置的下一个给插入新结点的下一个。
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
n.next=p.next;
p.next=n;
这两个方法主要是上面的插入方法中的头插和尾插的方法可以直接掉用add()方法
addLast(E e)在链表的最后一个位置添加一个元素
可复用add(int index,E e)方法
public void addFirst(E e) {
add(0,e);
}
addFirst(E e)在链表的第一个位置添加一个元素
复用add(int index ,E e)就可以了:
public void addLast(E e) {
add(size,e);
}
get(int index):通过下标返回数据:
首先对下标进行判定如果小标为0则是第一个结点中的元素,当下标等于szie-1是就是返回的是尾节点的元素,当然也应该注意你传入的的这个下标是否存在,做一个判断。
思路:根据传入的下标返回对应的元素:传入一个下标分三种情况:
分别是,得到第一个结点的元素
得到最后一个结点的元素.
根据传入的下标,遍历所有结点,找到对应结点元素,并且返回.
if(index<0||index>=size){
throw new IllegalArgumentException(“查找角标非法!”);
}
如果传入的下标符合要求,则从链表的头节点开始向后依次查找到相应的下标的节点,从节点中取出数据并且返回.代码如下:
if(index<0||index>=size){//判断是否合法
throw new IllegalArgumentException("查找角标非法!");
}
if(index==0){
return head.next.data;//下标为0则是头结点的下一个节点的数据
}else if(index==size-1){
return rear.data;//如果下标为元素个数,择返回尾节点元素数据,
}else{
Node p=head;
for(int i=0;i<=index;i++){//否则佛你个头结点开始一次向后找找到对应下标的节点,返回该结点元素.
p=p.next;
}
return p.data;
}
}
getFirst(),和方法getLast(),方法:的本质还是get(int index)方法反的服用,是两种特殊情况,但依旧可以使用get(int index);方法
getFirst()是得到第一个节点位置的数据可以复用get(int index)方法
public E getFirst() {
return get(0);
}
getLast()是得到最后一个节点位置的数据也可以复用get(int index)方法
public E getLast() {
return get(size-1);
}
set(int index, E e)方法与get(int index)方法类似,首先进行根据输入下标查找,得到节点后将对应结点的元改该为传入的元素:
替换前:
set(int index ,E e)方法和get(int index)有相似的思路,修改第一个结点的数据,修改最后一个结点的数据,和中间指定位置的数据.
:修改第一个结点的数据:其本质就是头节点的下一个结点将其数据域修改成传入的数据.
修改最后一个节点的数据,将尾结点的数据域进行变更.
如果是中间指定位置的元素,应该cong
替换后:
head.next.data=e;
}else if(index==size-1){//如果下标恒等于长度-
rear.data=e;//那么设置的就是尾节点中的元素,
}else{
Node p=head;
for(int i=0;i<=index;i++){//否则从头结点开始找到对应下标的结点,将新数据赋给对应结点的数据.
p=p.next;
}
p.data=e;//将新数据赋给对应结点的数据.
}
remove(int index);
删除并返回链表中指定角标index处的元素
删除也分为删除表头,删除表中,和删除表尾
删除方法:如果是删除头结点,将第一个及节点删除将第一个节点指向下一个节点的赋给都接点,就可以了
如果是删除尾节点:将最后一个一个节点删除,将最后节点的前一个节点的下一个指向空,尾指针向前移动:
如果是一般删除的话,就要从头到尾的找,根据你传入的下标,找到该下标的前一个元素,将要删除元素的下一个赋给删除元素的前一个元素。
在删除的元素之后,链表的长度随之减小,所以无论哪种删除都要是链表的长度-1;最后返回删除的节点的数据。
代码如下:
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;
p=null;
if(size==1){
rear=head;
}
}else if(index==size-1){//尾删
Node p=head;
res=rear.data;
while(p.next!=rear){
p=p.next; //循环遍历到最后一个
}
p.next=null;
rear=p;
}else{
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
Node del=p.next;
res=del.data;
p.next=del.next;
del.next=null; //将要删除的元素结点置为空;
del=null;
}
size--; //链表长度减1;
return res;
}
removeFirst()方法
其方法的具体实现,就是头删,我们在前面分析了头删的情况。,在这里可以进行对remove()方法的复用,
public E removeFirst() {
return remove(0);
}
removeLast()方法,亦可以调用remove()方法,,该方法的具体实现就是尾删(;)
public E removeLast() {
return remove(size-1);
}
clear();方法:清空链表中的元素。也就是一个回到一个空的链表的状态,这个时候,该链表的头指针,指向尾指针,都指向空,并且该量表的长度为空:
public void clear() {
head.next=null;
rear=head;
size=0;
}
以上就是链表实现的所有方法了。