1.特点:
1.基于链表数据结构(双向链表);
2.下标查询,时间复杂度为log2(n),二分查找;
3.线程不安全;
4.增删改效率高,查询效率比较低;
2.数组与链表结构的区别
数组:保证元素的有序性,可以基于下标精确查找,时间复杂度为O(1),适用于基于下标查询的场景,
链表:单向链表和双向链表,也可以基于下标查找,但是时间复杂度为O(n)=>它需要挨个节点挨个节点的找下去,因此查询效率较低,适用于增删改的场景,因为只需要修改引用的指针关系,而数组删除是把目标元素后的所有元素往前移动一位,这样的效率很低;
3.底层相关参数分析
1.first节点:整个链表里面的头节点(必须要:为了方便后期遍历数据知道从哪里开始)
2.last节点:整个链表里面的尾节点(其实可以不用记录);
transient Node<T> first;//没有prev节点的就是头节点;
transient Node<T> last;//没有next节点的就是尾节点;
3.item:节点元素
4.next:当前节点的下一个节点
5.prev:当前节点的上一个节点(从这两个参数就可以看出来LinkedList底层是双向链表而不是单向链表),那么我们就要注意在新增节点的时候需要指定两者之间的关系(需要同时绑定前者的next节点和后者的prev节点才能对应两者的关系)
4.add方法
因此在知道相关参数以及它底层是双向链表之后,我们再来思考它的add方法需要做什么操作?
1.创建一个新节点;
2.将新节点的prev节点指向原链表的last节点;
3.新节点的next节点设置为null;
4.链表的尾节点设置为新的这个节点
5.判断原链表的last节点是否为空,如果为空则说明这一次新增操作是当前链表的第一次新增,则将新节点设置为头节点,如果不为空则需要完成两者关系的最后一次绑定即原链表尾节点的next节点指向新节点
然后我们再来看看底层是不是这样实现的:
调用add方法,然后它将参数直接传给了下面的这个linkLast方法;
void linkLast(E e) {
final Node<E> l = last;//将当前链表的尾结点记录下来
final Node<E> newNode = new Node<>(l, e, null);//创建新节点
last = newNode;//指定新节点为链表的尾结点
if (l == null)//如果尾结点为空,说明链表为空是第一次新增,则将新节点指定为头节点
first = newNode;
else//否则再将原链表尾节点的next节点指向新节点
l.next = newNode;
size++;
modCount++;
}
我们再看在第二步创建新节点的时候调用的有参构造器长什么样子:
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;
//下一个节点的指向(null)
this.next = next;
//上一个节点的指向(原链表的last节点)
this.prev = prev;
}
}
底层源码确实也是这样实现的。
5.get方法(折半查找原理)
链表中如果使用普通的根据下标查找的方法,也需要从头节点挨个节点的往下遍历查找,直到找到目标节点,因此时间复杂度为O(n),效率特别低,因此引入了二分查找的算法;
简单理解二分查找:
我们把整个链表的长度除以2取到中间值,然后拿我们传过去的索引值与这个中间值进行对比,根据判断的结果来决定是在前半段查找还是后半段查找,这样就大大的减少了查询的次数,从而提高了查询效率;
例如我们一个链表的长度为10,我们需要查找下标为8的元素,按照之前的方法,我们需要遍历9次才能得到;
而使用二分查找的方法,首先通过10除以二得到中间值5,判断下标值8大于5,则在后半段进行查找,因此从后往前查找,只需要查找3次则得到目标元素,效率大大提高;
源码
Node<E> node(int index) {
// assert isElementIndex(index);
//该算法就是二分查找的算法
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
我们再来分析一下源码的实现逻辑:
1.首先判断传入的索引值与中间值的对比结果;
2.若我们的索引值小于中间值,则说明在前半段进行查找,那么就正常遍历查找到索引前的位置节点,再获取到它的next节点即为目标节点;
3.若我们的索引值大于中间值,说明索引在后半段,那么就从后往前查找,直到遍历到索引前的节点,取到该节点的prev节点即为目标节点
6.remove方法
我们已经知道了linkedList的底层是双向链表结构,因此我们删除就需要改引用:即被删除的节点的prev节点和next节点需要相互引用起来;
源码
无论是根据下标删除还是根据元素值删除,最终它都会获取到具体的节点然后进入下面这个核心方法中:
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
在这个方法中为了避免大家混淆,我们一步一步看,根据if else分成两个板块看;
首先它拿到了我们被删除节点的所有数据:元素值,前节点,后节点;
我们先看第一个if else
1.先对前节点操作
1.1 判断前节点是否为空,如果为空则说明被删除的节点为头节点,因此我们只需要将被删除节点的next节点设置为头节点即可;
1.2 若前节点不为空,则正常删除,即被删除节点的prev节点的next节点设置为被删除节点的next节点;
再看第二个if else
2.对后节点操作
2.1 判断后节点是否为空,如果为空则说明被删除的节点为尾节点,因此我们只需要将被删除节点的prev节点设置为尾节点即可;
2.2 若后节点不为空,则正常删除,即被删除节点的next节点的prev节点设置为被删除节点的prev节点;
3.将被删除节点的元素值和相关prev节点以及next节点设置为null,告诉GC去清理垃圾
4.总数量size-1;
这里可能稍微有一点绕,建议大家画图去理解。