Java集合:LinkedList
初识LinkedList
上一篇中讲解了ArrayList,本篇文章讲解一下LinkedList的实现。
LinkedList是基于链表实现的,所以先讲解一下什么是链表。链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据以外,还存储有其下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放有其前一个存储单元的地址),每次查找数据的时候,通过某个存储单元中的下一个存储单元的地址寻找其后面的那个存储单元。
这么讲可能有点抽象,先提一句,LinkedList是一种双向链表,双向链表我认为有两点含义:
1、链表中任意一个存储单元都可以通过向前或者向后寻址的方式获取到其前一个存储单元和其后一个存储单元
2、链表的尾节点的后一个节点是链表的头结点,链表的头结点的前一个节点是链表的尾节点
LinkedList既然是一种双向链表,必然有一个存储单元,看一下LinkedList的基本存储单元,在jdk1.8中,它是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上的答案
关注点 | 结论 |
是否允许空 | 是 |
是否允许重复数据 | 是 |
是否有序 | 是 |
线程安全 | 否 |
下面的源码来自JDK1.8版本
添加元素
/** * 把具体的元素添加到链表最后 * * 这个方法与addLast()方法是一样的 * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { // 获取当前最后一个节点 final Node<E> l = last; // 新建节点 final Node<E> newNode = new Node<>(l, e, null); // last指向新节点 last = newNode; // 如果一开始是空的链表 if (l == null) first = newNode; else // 原链表的最后一个节点的next指向新节点 l.next = newNode; size++; modCount++; }
在某个位置中插入元素
/** * Inserts the specified element at the specified position in this list. * Shifts the element currently at that position (if any) and any * subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { checkPositionIndex(index); // 如果位置刚好为size,那么就直接连接到最后 if (index == size) linkLast(element); else // 使用前插法 linkBefore(element, node(index)); }
查找元素
/** * Returns the element at the specified position in this list. * * @param index index of the element to return * @return the element at the specified position in this list */ public E get(int index) { // 检查下标范围 checkElementIndex(index); return node(index).item; } /** * Returns the (non-null) Node at the specified element index. */ Node<E> node(int index) { // 如果index小于size/2,那么就从链头开始查找 // 否则从链尾开始查找 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; } }
这段代码就体现出了双向链表的好处了。双向链表增加了一点点的空间消耗(每个Entry里面还要维护它的前置Entry的引用),同时也增加了一定的编程复杂度,却大大提升了效率。
由于LinkedList是双向链表,所以LinkedList既可以向前查找,也可以向后查找,第6行~第12行的作用就是:当index小于数组大小的一半的时候(size >> 1表示size / 2,使用移位运算提升代码运行效率),向后查找;否则,向前查找。
这样,在我的数据结构里面有10000个元素,刚巧查找的又是第10000个元素的时候,就不需要从头遍历10000次了,向后遍历即可,一次就能找到我要的元素。
删除元素
看完了添加元素,我们看一下如何删除一个元素。和ArrayList一样,LinkedList支持按元素删除和按下标删除,前者会删除从头开始匹配的第一个元素。
/** 从头开始遍历每个节点直到第一次找到目标元素 */ public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } /** * 删除一个非空的节点x */ E unlink(Node<E> x) { // 此处x!=null final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev;
// 把x相关的引用都变为null,方便gc回收 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; }
LinkedList和ArrayList的对比
老生常谈的问题了,这里我尝试以自己的理解尽量说清楚这个问题,顺便在这里就把LinkedList的优缺点也给讲了。
1、顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象(Node)出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList
2、基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Node的前置和后继,如果一个LinkedList中的Node非常多,那么LinkedList将比ArrayList更耗费一些内存
3、数据遍历的速度,看最后一部分,这里就不细讲了,结论是:使用各自遍历效率最高的方式,ArrayList的遍历效率会比LinkedList的遍历效率高一些,因为前者是顺序存储的,而后者只能通过指针去逐个索引查找。
4、有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:
(1)LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Node的引用地址
(2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址
所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。
从这个分析看出,如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你删除、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。