我们为什么需要LinkedList?
LinkedList并不是通过数组实现的,而是一种双向链表的结构来实现。这种结构的特点为:查询效率低,增删效率高,线程不安全
所有,增加或删除元素较多时,我们选用LinkedList。
基本实现
双向链表
LinkedList是通过双向链表实现的,首先,我们得了解双向链表。双向链表的结构如下图:
链表是一个一个的节点组成的,在双向链表中,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。
代码实现
双向链表中的节点实现源码如下:
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类中有如下声明:
transient int size = 0; //用于记录元素个数,初始值是0
transient Node<E> first; //前指针,用于记录第一个节点
transient Node<E> last; //后指正,用于记录最后一个节点
增、刪、改、查
容器嘛,用来存储数据的,那就离不开”增、刪、改、查“这里四个字。那就来逐一看LinkedList是如何实现实现的。
增
双向链表中增加数据,就是在最后一个节点,再挂上一个节点。旧最后节点的后指正指向新节点,新节点的前指针指向旧节点。源码:
public boolean add(E e) {
linkLast(e); //调用linkLast方法
return true;
}
void linkLast(E e) {
final Node<E> l = last; //取出目前最后一个节点
final Node<E> newNode = new Node<>(l, e, null); //初始化新节点
last = newNode; //新节点,就成为了最后一个节点
if (l == null) //最后一个节点为null,说明是此时还没有节点
first = newNode; //那么此节点,也同时为第一个节点
else
l.next = newNode; //旧节点的后指针,指向新节点
size++; //用于记录元素个数的size加1
modCount++;
}
删
双向链表的删除,其实就是移除,将节点移除链表。如果是移除中间的节点,则:被移除节点前一个节点的后指针,指向被移除节点的后一个节点;被移除节点后一个节点的前指针,指向被移除节点的前一个节点。源码如下:
public E remove(int index) {
checkElementIndex(index); //检查确保输入的索引为正常合理值
return unlink(node(index)); // 调用unlink方法
}
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) { //前一个节点为null,说明被移除节点是头节点
first = next; //被移除后,头节点应该为被移除节点的后一个节点
} else {
prev.next = next; //被移除节点前一个节点的后指针,指向被移除节点的后一个节点
x.prev = null; //被移除节点的前指针置空
}
if (next == null) { //前一个节点为null,说明被移除节点是尾节点
last = prev; //被移除后,头节点应该为被移除节点的前一个节点
} else {
next.prev = prev; //被移除节点后一个节点的前指针,指向被移除节点的前一个节点
x.next = null; //被移除节点的后指针置空
}
x.item = null; //被移除节点的存储内容置空
size--; //用于记录元素个数的size减1
modCount++;
return element;
}
改
LinkedList的增删容易,改和查就比较麻烦了(主要是执行次数上升),需要循环多次来找到指定位置,再进行操作。源码如下:
public E set(int index, E element) {
checkElementIndex(index); //检查确保输入的索引为正常合理值
Node<E> x = node(index); //调用node方法,找到指定节点
E oldVal = x.item;
x.item = element; //修改节点存储的内容
return oldVal;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //2分法
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;
}
}
查
“改”中,其实就是先查后改的,所有和改类似,查的源码如下:
public E get(int index) {
checkElementIndex(index); //检查确保输入的索引为正常合理值
return node(index).item; //返回节点里存储的数据内容
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //2分法
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;
}
}
toString
源码与ArrayList大致相似,主要是对双相链表的遍历,我这里手写了遍历,便于理解:
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
Node temp = first; //从头节点开始
while(temp!=null){ //一直取到空为止
sb.append(temp.element+",");
temp = temp.next; //取该节点的下一个节点,继续循环
}
sb.setCharAt(sb.length()-1, ']');
return sb.toString();
}