变秃变强

问题

  1. 平时大家都在说 ArrayList 查找快,增删慢;LinkedList 查找慢,增删快,但是为什么呢?
  2. LinkedList 底层实现,如果让你实现一个简单的 LinkedList,你会怎么实现?
  3. 大家都知道 LinkedList 底层是双向链表,那么你觉得适用于哪些场景?

分析源码

首先还是想来看类注释,总结出以下几点:

  1. 双向链表实现了 ListDeque 接口,实现了所有可选的 List 操作,允许元素为 null;
  2. 是线程不安全的,List list = Collections.synchronizedList(new LinkedList(...)) 来实现线程安全;
  3. 不能保证迭代器的 fail-fast 行为,在不同步的并发情况下,不可能做出严格的保证。fail-fast 仅应用于检测错误。

整体结构

双向链表

双向链表,又称为双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表

链表中的元素叫 Node,可以看下源码中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

从头部新增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void linkFirst(E e) {
// 头结点引用
final Node<E> f = first;
// 创建新增的节点
final Node<E> newNode = new Node<>(null, e, f);
// 因为是加到头部,所以新增的就是新的头节点
first = newNode;
// 如果头结点为空,那么表示头尾节点是同一个,都是 null
if (f == null)
last = newNode;
else
// 头结点的上一个节点指向新节点
f.prev = newNode;
size++;
modCount++;
}

查找

尾部追加跟头部新增类似,这里就不再讲了。我觉得链表中实现的二分查找的思想,我们是可以借鉴的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Node<E> node(int index) { // index 需要查找的下标
// assert isElementIndex(index);
// size >> 1 = size / 2,如果 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;
}
}

删除

以删除对应下标的 Node 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public E remove(int index) {
// 检查索引是否在队列中
checkElementIndex(index);
// 根据索引找到对应的 Node
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
// 当前删除Node的值
final E element = x.item;
// 当前Node的下一个节点
final Node<E> next = x.next;
// 当前Node的前一个节点
final Node<E> prev = x.prev;

// 如果前一个节点为null,说明当前节点就是头节点,那么把头节点设置为当前节点的下一个节点
// 反之,前一个节点的下一个节点指向当前节点的下一个节点
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

// 如果尾结点为 null,说明当前节点就是尾结点,那么把尾结点设置为当前节点的前一个节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

迭代器

因为 LinkedList 需要实现双向的访问,Iterator 满足不了。因为 Iterator 只能从前往后遍历,Java 提供了一个新的接口 ListIterator,提供了向前和向后迭代的方法:

迭代顺序 方法
从前往后迭代的方法 hasNext()、next()、nextIndex()
从后往前迭代的方法 previous()、previousIndex()、hasPrevious()

其实我看了下 ArrayList 也是有实现 ListIterator 接口的,只不过平时没用到,例如我们也可以从前往后查找:

1
2
3
4
5
6
7
8
9
10
11
public void testListIterator(){
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
ListIterator<String> listIterator = list.listIterator(2);
while (listIterator.hasPrevious()){
String previous = listIterator.previous();
System.out.println(previous);
}
}

LinkedList 里的 ListIterator 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned = null; // 迭代过程中上一次返回的Node
private Node<E> next; // 下一个Node
private int nextIndex; // 下一个索引
private int expectedModCount = modCount;
public E next() {
// 检查版本号是否有误
checkForComodification();
// 二次检查,是否还有下一个Node
if (!hasNext())
throw new NoSuchElementException();
// 第一次迭代的时候上一次返回值等于next节点
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
}

好的,现在可以回答文中前面几个问题了。

  1. ArrayList 底层是数组,LinkedList 底层是双向链表
  2. 对于插入 (add) 和 remove (删除),ArrayList 需要移动目标节点后面的节点 ,System.arraycopy 用来移动节点(native方法)。而 LinkedList 只需要修改目标节点对应的 next 或 prev 属性即可。
  3. 对于查找操作,因为 ArrayList 是拿数组存的,所以直接根据索引就能取到对应的数据。而 LinkedList 需要从前往后或者从后往前迭代,查找目标数据

PS:对于顺序插入,也就是 ArrayList 不需要扩容,并且是最后一个节点插入,那么就不需要移动节点,因此效率上也不会比 LinkedList 差

最后附上自己实现的 LinkedList 代码地址