Java LinkedList【笔记】
LinkedList
LinkedList 适用于要求有顺序,并且会按照顺序进行迭代的场景,依赖于底层的链表结构
LinkedList基本结构
LinkedList 底层数据结构是一个双向链表
链表每个节点叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置
双向链表的头节点(first)的前一个节点是 null
双向链表的尾节点(last)的后一个节点是 null
当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null
因为是个双向链表,只要机器内存足够强大,是没有大小限制的
链表中的元素叫做 Node,初始化参数的顺序为前一个节点,本身节点值,后一个节点
LinkedList追加节点(新增节点)
在LinkedList追加节点的时候,可以选择追加到链表头部,也可以选择追加到链表尾部,add是默认为尾部追加,addfirst是从头部追加
从头部开始追加(add)
简单来说,尾部追加节点只需要把指向位置修改下就行了
具体操作,我们首先需要将尾节点的数据暂时存储起来,然后建立一个新的节点,初始化,需要注意,新节点的前一个节点的值为尾节点值,新增节点的后一个节点为nul,然后将新建的节点追加到尾部,如果链表为空,头部和尾部都是同一个节点,都是新建的节点,否则把前尾节点的下一个节点指向当前尾节点
源码:
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
l.next = newNode;
size++;
modCount++;
}
从头部追加的方法(addfirst)
将头节点赋值给临时变量,然后新建节点,前一个节点指向null,新建节点的下一个节点的值为头节点的值,然后将新建节点变成头节点,如果头节点为空,那么就是链表为空,头尾节点就是一个节点,此时上一个头节点的前一个节点就指向当前节点
源码:
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
通过这两个方法的对比,可以发现头部追加节点和尾部追加节点是非常相像的,前者是移动头节点的 prev 指向,后者是移动尾节点的next指向
LinkedList节点删除
和追加有一些类似,也是可以选择从头部删除或者是从尾部删除,删除的时候会把节点的值以及前后指向节点都变为null,这样有利于垃圾回收(GC)
从头部删除的话,首先我们先拿出头节点的值来作为方法的返回值,拿出头节点的下一个节点来,将前后设为null帮助垃圾回收,然后头节点的下一个节点成为头节点,如果next为空,则说明链表为空,如果链表不为空,则将头节点的下一个节点指向null,然后修改一下链表大小
源码:
private E unlinkFirst(Node<E> f) {
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null;
first = next;
if (next == null)
last = null;
null
else
next.prev = null;
size--;
modCount++;
return element;
}
可以发现,链表结构的节点新增和删除都只需要将前后节点的指向修改下就可以,这说明LinkedList的新增和删除速度很快
LinkedList节点查询
链表的查询是比较慢的,而在LinkedList中进行节点查询不是按照从头循环到尾的方法,而是用的简单二分法,先看index是在链表的前半部分还是后半部分,如果是前半边,就从头找,如果是后半边,就从尾找,这样可以提高一些性能,而且因为链表的性质,即只能从第一个或者最后一个去访问其他的元素,所以简单二分法已经是最优解了
源码:
Node<E> node(int 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;
}
}
LinkedList中的接口的新增方法
LinkedList迭代器
LinkedList使用ListIterator迭代接口来实现双向的迭代访问,这个接口提供了向前以及向后的迭代方法
从头到尾的方向的迭代
首先判断一下有没有下一个元素,如果下一个节点的索引小于链表的大小,那么就说明有下一个元素,然后我们取一个元素,看一下版本号有无变化,然后再检查一遍,next是当前的节点,在上一次执行next()方法的时候被赋值的,如果是第一次执行,那么就是在初始化迭代器的时候被赋值的,然后next是下一个节点,为下一次的迭代做准备
源码:
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
从尾到头的迭代
首先我们要确定上次节点的索引位置,如果上次节点索引位置大于0,那么就代表有节点可以迭代,然后我们取前一个节点,同样检查版本号,在next为空的时候,有两个可能,第一个,说明这是第一次迭代,取尾节点,第二个,上次操作的时候把尾节点删掉了,而在next不为空的时候,这就说明已经发生过迭代,直接去前一个节点就可以了,然后改变索引位置
源码:
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
一些问题:
ArrayList 和 LinkedList 有何异同?
不同:
底层数据结构方面
ArrayList 底层是数组
LinkedList 底层是双向链表
应用场景方面
ArrayList 更适合于快速的查找匹配,不适合频繁新增删除
LinkedList 更适合于经常新增和删除,对查询反而很少的场景
相同:
最大容量
ArrayList 有最大容量的,为 Integer 的最大值,大于这个值 JVM 是不会为数组分配内存空间的
LinkedList 底层是双向链表,理论上可以无限大,但是实际上,LinkedList 实际大小用的是 int 类型,这也说明了 LinkedList 不能超过 Integer 的最大值,不然会溢出
** null 值**
ArrayList 允许 null 值新增,也允许 null 值删除
LinkedList 新增删除时对 null 值没有特殊校验,是允许新增和删除的。
线程安全
当两者作为非共享变量时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题的,只有当两者是共享变量时,才会有线程安全问题