本文简单介绍下什么是LinkedList以及链表的结点和双向链表结构以及怎样用Java简单重写LinkedList类,目的在于重写的LinkedList类实现与JDK提供的LinkedList类中基本功能相同
前言
本项目使用的是Maven项目,同时使用的是Junit框架进行测试。如果是普通的Java项目,需要自己拷贝代码并自己写主函数方法进行测试。
什么是LinkedList
LinkedList
LinkedList类是List接口的一个具体实现类,用于创建链表数据结构来存放数据,相比于List接口的另一个具体实现类ArraysList,LikedList在插入或删除元素时具有更好的性能,但在查找某一元素时,LinkedList的性能就不如ArraysList了。LinkedList基本用法与ArraysList相同,但LinkedList具有一些自己独特的方法,所以当我们使用LinkedList自身独有的方法时(比如addFirst、addLast等),要注意如果是用List接口去定义的LinkedList对象,我们需要把对象向下转型进行强制类型转换,才能调用LinkedList的独有的方法。
比如:
List<String> list=new LinkedList<String>();
LinkedList<String> list1=(LinkedList<String>) list;
list1.addFirst("d");
链表结点
LinkedList的底层是基于双向链表,提到双向链表就得先说下构成双向链表的结点。在链表的数据结构中,链表中的每一个元素称为“结点”,单链表中每个结点都应包括两个部分:一个是需要用到的实际数据data;另一个就是存储下一个结点地址的指针,即数据域和指针域;而双向链表中,每个结点包括存储其上一个结点地址的指针,用到的数据data和存储下一个结点的指针。数据结构中的每一个数据结点对应于一个存储单元,这种储存单元称为储存结点,也可简称结点。
单向链表结点结构(data为实际存放的数据,next为存放下一个结点地址的指针)如图
双向链表结点结构(prev是存放上一个结点地址的指针,data为实际用到的数据,next为存放下一个结点地址的指针)如图
双向链表结点在Java中的构造代码
JDK底层提供的源码
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
我自己重写LinkedList时构造的双向链表结点代码
/*设计结点,作为一个内部类封装在LinkedList类里仅供LinkedList本类使用*/
private static class Node {
Object data;//结点中实际存放的元素数据
Node prev;//上一个结点(直接前驱)的引用
Node next;// 下一个结点(直接后继)的引用
Node(Node prev,Object data,Node next){
this.prev=prev;
this.data=data;
this.next=next;
}
}
双向链表结构
LinkedList存储是双向链表结构,双向链表是链表的一种,双向链表又分为双向循环链表和普通的双向链表,JDK中的LinkedList类是普通的双向链表不涉及循环,双向链表中每个数据结点在存放实际用的数据的同时还都有两个指针分别这个结点的直接前驱和直接后继,所以从双向链表的任意一个结点都能很容易直接访问它的前驱结点和后继结点。
Java中双向链表结构如图
我所重写的LinkedList类双向链表结构的Java参考代码
package list.ext;
import list.iface.List;
/**
* @author 作者 水寒轩
* @version 1.0
创建时间:2019年12月4日 上午9:06:34
* 类说明 重写LinkedList方法
*/
public class LinkedList implements List {
/*设计结点*/
private static class Node {
/*设计双向链表结点,代码参考本文结点的部分*/
}
private Node firstNode=null;//链表中的首节点
private Node lastNode=null;//链表中的尾节点
private int size=0;//链表长度
/*以下是对链表操作的各类方法*/
方法1
方法2
.....
}
关于List接口
LinkedList是List接口的一种具体实现类,所以我们在重写LinkedList时必须实现List接口里的方法。
我们可以通过查看参考JDK提供的List接口的方法,自己创建一个List接口,我们自己创建的List接口中要包括一些JDK提供的List接口常用的方法(我们可以在List接口中将其定义为抽象方法),然后我们对自己写的List接口进行实现,目的在于通过我们自己写的List接口实现与JDK中提供的List接口相同的基本功能方法。
List接口代码
package list.iface;
/**
* @author 作者 水寒轩
* @version 1.0
创建时间:2019年12月4日 上午8:09:59
* 类说明 List接口
*/
public interface List{
/**
* @return the size 获取集合中元素的数量
*/
public abstract int size();
/**
* @param object 向集合中添加元素
* @return
*/
public abstract boolean add(Object object);
/**
* @param index 指定位置下标 向集合中指定插入某个元素的目标位置
* @param object 要向集合插入的元素
* @return
*/
public abstract void add(int index, Object object);
/**
* @param anotherExtList 将anotherExtList集合追加到本集合的最后
*/
public abstract void addAll(List anotherExtList);
/**
* @param index 移除集合中指定index下标位置的元素
*/
public abstract void remove(int index);
/**
* 清空集合
*/
public abstract void clear();
/**
* @param index 输入集合中元素值的index位置
* @return 返回集合中index位置上元素的的值
*/
public abstract Object get(int index);
/**
* @return 返回数组对象
*/
public abstract Object[] toArray();
/**
* 打印输出集合中元素的值
*/
public abstract void printList();
/**
*
* @param index 输入集合中要修改的元素的指定index下标位置
* @param object 修改后的元素
*/
public abstract void set(int index, Object object);
/**
* @param anotherExtArrayList 将anotherList集合追加到本集合的指定index位置
*/
public abstract void addAll(int index,List anotherExtList);
}
重写LinkedList类方法
本部分会讲解部分重点方法代码,其余方法解析请参考之后的重写LinkedList类的具体代码。
基础方法
这里的基本方法不是简单的add一类的方法,而是对LinkedList操作的一些基本方法,特别是一些封装在类内部仅供LinkedList本类调用的方法是实现LinkedList类的一些基础功能方法的先决条件,十分重要。
判断下标是否合法
首先,当我们打算给集合传一个下标,进行插入或者删除的时候,我们必须考虑传入的下标是否合法,一个集合合法的下标范围是在0到这个集合的size-1,超过这个范围的下标都是非法操作,所以我们需要一个方法去判断这个下标是否合法,合法时继续操作,非法就抛出异常。具体封装方法实现请参考代码注释,封装方法代码如下:
/*调用isElementIndex方法,判断输入下标是否合法,不合法时抛出异常*/
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+this.size);
}
/*判断输入下标是否合法,供checkElementIndex方法调用,不合法时由checkElementIndex方法抛出异常*/
private boolean isElementIndex(int index) {
/*表达式index >= 0 && index < size成立返回true,不成立返回false*/
return index >= 0 && index < size;
}
根据下标找结点的内部封装方法
关于根据下标找结点的内部封装方法,之所以需要这种方法,是因为LinkedList与ArrayList结构不同,ArrayList存储结构的本质是一个数组,是在内存地址中开辟的一组地址连续的空间单元,如图:
为什么ArrayList查找某一元素性能好而添加或删除性能差,是因为ArrayList的首元素的内存地址我们是已知的,当我们查找某个下标的时候,只需要根据
首元素地址+下标×每个元素占用的内存空间大小,就能很快的精确定位到该元素的位置,以下标为i的元素每个元素占n个字节内存空间大小举例,我要查找它在ArrayList的位置,只需要根据首地址0x00XX+i*n就能查找到下标为i的元素位置,但是在添加,ArryaList必须再在数组中开辟一个新的空间存放新的数组,新的数组空间要比原先的大,然后再在把原先数组元素拷贝进新数组里,再在指定位置添加元素,原指定位置元素及其之后的所有元素后移;删除的时候是要先把指定元素之后的元素前移,开辟一个比原先数组小的新数组(可以先理解为比原先数组长度小1位),再把原先数组从头拷贝原先数组长度减一个元素,这样才实现了插入和删除操作。而LinkedList存储方式在内存中是无序的所以首元素地址+下标×每个元素占用的内存空间大小的方法就不能够使用,只能通过遍历链表来判断下标index元素结点在链表中的位置,也可以把这个过程理解为就是给链表中的元素结点打下标(这也是为什么LinkedList查找某一元素性能不如ArrayList的根本原因,因为每次我们去查找一个元素都得对链表进行遍历,但是在插入或删除时,LinkedList虽然需要进行遍历,但是却不会发生元素的位移,也不需要开辟新的空间用来存放修改后的元素数据)所以我们需要一个判断结点下标的方法来实现这个过程。
判断结点下标的方法有两种,首先是第一种,从头开始遍历,一个一个查,代码如下:
// /*根据下标找节点:从头遍历*/
private Node node(int index) {
//先判断index是否合法
checkElementIndex(index);
int tag=0;
/*用来循环统计在链表中从头查找结点所经历的结点个数
也可以理解为是手动给结点做下标,
比如查第一个结点经历了0个结点,所以第一个结点的tag是0;
查找第二个结点经历了1个结点,所以第二个结点的tag是1以此类推,
可以理解成结点的下标*/
Node tempNode=null;//用来指向查找到的结点返回
/*从首结点往后遍历*/
for(Node n=this.firstNode;n!=null;n=n.next) {
/*判断查找的第几个结点是index*/
if(tag==index) {
temp=n;
break;
}
tag++;
}
return temp;
}
然后是第二种,二分查找法来判断结点下标,先跟长度的中间数比较,如果比其小就从头开始查找,比其大就从尾元素开始查找,直到查找到为止,相比第一种方法,遍历元素是第一种方法的一半,时间复杂度也是第一种的一半,程序效率是第一种方法的二倍,代码如下:
/*根据下标找节点:二分法查找*/
/*优点:是从头查找的普通方法的时间复杂度的一半,程序效率是普通从头遍历方法的二倍*/
private Node node(int index) {
//先判断index是否合法
checkElementIndex(index);
/*集合长度右移一位,相当于除以2*/
// 从效率上看,使用移位指令有更高的效率,因为移位指令占2个机器周期,而乘除法指令占4个机器周期。
// 从硬件上看,移位对硬件更容易实现,所以会用移位
if(index<(size>>1)) {
/*前半部分从头找*/
Node node=this.firstNode;
for(int i=0;i<index;i++) {
node=node.next;
}
return node;
}else {
/*后半部分从末尾找*/
Node node=this.lastNode;
for(int i=size-1;i>index;i--) {
node=node.prev;
}
return node;
}
}
根据下标获得元素的方法,只需要调用根据下标判断结点的方法找到相应的结点再返回结点的值即可,代码如下:
/*根据下标获取链表元素*/
public Object get(int index) {
/*先调用node(index)方法,根据下标查找节点,再返回节点的值*/
return this.node(index).data;
}
添加元素方法
addFirst、addLast以及add方法
addFirst和addLast是LinkedList的独有方法,而add方法是List接口提供的方法由LinkedList去具体实现,其中,add方法功能与addLast方法一致,在Java代码中add也是直接调用的addLast方法进行实现,所以不再进行详细讲解。
先讲解下addFirst和addLast方法,顾名思义,addFirst是添加首元素而addLast是添加尾元素,接下来分别讲解。
addFirst方法
首先先来讲解addFirst,分为空表状态下添加和非空表状态下添加
空表状态下添加首元素:
由于表中只有一个添加元素,所以链表首结点结点对象和尾结点结点对象都指向这一个元素结点,这个元素结点的prev和next都为null,链表长度从0变为1
非空表状态下添加首元素:
添加前
添加时,让原首元素结点的prev指向要添加的结点,要添加的结点addNode的next指向原首元素结点,过程如图
最后再让链表的首结点结点对象指向添加的结点,然后让链表的长度加1,添加首结点的过程就完成了。
重写后的addFirst方法实现代码
/*在LinkedList首元素位置添加元素*/
public void addFirst(Object object) {
/*构建结点用来存放元素数据*/
Node node=new